Unverified Commit ccc3dd96 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Switch to assemble API for dart2js (#41447)

parent 3df57a71
......@@ -282,10 +282,9 @@ Future<void> _runBuildTests() async {
}
// Web compilation tests.
await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web'), path.join('lib', 'main.dart'));
// Should fail to compile with dart:io.
// Should not fail to compile with dart:io.
await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web_compile_tests'),
path.join('lib', 'dart_io_import.dart'),
expectNonZeroExit: true,
);
print('${bold}DONE: All build tests successful.$reset');
......
......@@ -4,21 +4,17 @@
// ignore_for_file: implementation_imports
import 'dart:async';
import 'dart:convert'; // ignore: dart_convert_import
import 'dart:io'; // ignore: dart_io_import
import 'dart:isolate';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:archive/archive.dart';
import 'package:build/build.dart';
import 'package:build_config/build_config.dart';
import 'package:build_modules/build_modules.dart';
import 'package:build_modules/builders.dart';
import 'package:build_modules/src/module_builder.dart';
import 'package:build_modules/src/platform.dart';
import 'package:build_modules/src/workers.dart';
import 'package:build_runner/build_runner.dart' as build_runner;
import 'package:build_runner_core/build_runner_core.dart' as core;
import 'package:build_test/builder.dart';
......@@ -26,10 +22,7 @@ import 'package:build_test/src/debug_test_builder.dart';
import 'package:build_web_compilers/build_web_compilers.dart';
import 'package:build_web_compilers/builders.dart';
import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as path; // ignore: package_path_import
import 'package:scratch_space/scratch_space.dart';
import 'package:test_core/backend.dart';
const String ddcBootstrapExtension = '.dart.bootstrap.js';
......@@ -248,12 +241,8 @@ class FlutterWebEntrypointBuilder implements Builder {
@override
Future<void> build(BuildStep buildStep) async {
if (release || profile) {
await bootstrapDart2Js(buildStep, flutterWebSdk, profile);
} else {
await bootstrapDdc(buildStep, platform: flutterWebPlatform,
skipPlatformCheckPackages: skipPlatformCheckPackages);
}
await bootstrapDdc(buildStep, platform: flutterWebPlatform,
skipPlatformCheckPackages: skipPlatformCheckPackages);
}
}
......@@ -422,116 +411,6 @@ Future<void> main() async {
};
}
Future<void> bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk, bool profile) async {
final AssetId dartEntrypointId = buildStep.inputId;
final AssetId moduleId = dartEntrypointId.changeExtension(moduleExtension(flutterWebPlatform));
final Module module = Module.fromJson(json.decode(await buildStep.readAsString(moduleId)));
final List<Module> allDeps = await module.computeTransitiveDependencies(
buildStep,
throwIfUnsupported: true,
skipPlatformCheckPackages: skipPlatformCheckPackages,
)..add(module);
final ScratchSpace scratchSpace = await buildStep.fetchResource(scratchSpaceResource);
final Iterable<AssetId> allSrcs = allDeps.expand((Module module) => module.sources);
await scratchSpace.ensureAssets(allSrcs, buildStep);
final String packageFile = _createPackageFile(allSrcs, buildStep, scratchSpace);
final String dartPath = dartEntrypointId.path.startsWith('lib/')
? 'package:${dartEntrypointId.package}/'
'${dartEntrypointId.path.substring('lib/'.length)}'
: dartEntrypointId.path;
final String jsOutputPath =
'${path.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}'
'$jsEntrypointExtension';
final String flutterWebSdkPath = flutterWebSdk;
final String librariesPath = path.join(flutterWebSdkPath, 'libraries.json');
final List<String> args = <String>[
'--libraries-spec="$librariesPath"',
if (profile)
'-O1'
else
'-O4',
'-o',
'$jsOutputPath',
'--packages="$packageFile"',
if (profile)
'-Ddart.vm.profile=true'
else
'-Ddart.vm.product=true',
dartPath,
];
final Dart2JsBatchWorkerPool dart2js = await buildStep.fetchResource(dart2JsWorkerResource);
final Dart2JsResult result = await dart2js.compile(args);
final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension);
final File jsOutputFile = scratchSpace.fileFor(jsOutputId);
if (result.succeeded && jsOutputFile.existsSync()) {
final String rootDir = path.dirname(jsOutputFile.path);
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);
final AssetId jsSourceMapId =
dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension);
await _copyIfExists(jsSourceMapId, scratchSpace, buildStep);
} else {
log.severe(result.output);
}
}
Future<void> _copyIfExists(
AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async {
final File file = scratchSpace.fileFor(id);
if (file.existsSync()) {
await scratchSpace.copyOutput(id, writer);
}
}
/// Creates a `.packages` file unique to this entrypoint at the root of the
/// scratch space and returns it's filename.
///
/// Since mulitple invocations of Dart2Js will share a scratch space and we only
/// know the set of packages involved the current entrypoint we can't construct
/// a `.packages` file that will work for all invocations of Dart2Js so a unique
/// file is created for every entrypoint that is run.
///
/// The filename is based off the MD5 hash of the asset path so that files are
/// unique regarless of situations like `web/foo/bar.dart` vs
/// `web/foo-bar.dart`.
String _createPackageFile(Iterable<AssetId> inputSources, BuildStep buildStep, ScratchSpace scratchSpace) {
final Uri inputUri = buildStep.inputId.uri;
final String packageFileName =
'.package-${md5.convert(inputUri.toString().codeUnits)}';
final File packagesFile =
scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName));
final Set<String> packageNames = inputSources.map((AssetId s) => s.package).toSet();
final String packagesFileContent =
packageNames.map((String name) => '$name:packages/$name/').join('\n');
packagesFile .writeAsStringSync('# Generated for $inputUri\n$packagesFileContent');
return packageFileName;
}
/// Returns whether or not [dartId] is an app entrypoint (basically, whether
/// or not it has a `main` function).
Future<bool> _isAppEntryPoint(AssetId dartId, AssetReader reader) async {
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vmservice;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../application_package.dart';
import '../base/common.dart';
......@@ -20,6 +21,7 @@ import '../globals.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../resident_runner.dart';
import '../web/chrome.dart';
import '../web/web_device.dart';
import '../web/web_runner.dart';
import 'web_fs.dart';
......@@ -170,7 +172,7 @@ class ResidentWebRunner extends ResidentRunner {
initializePlatform: debuggingOptions.initializePlatform,
hostname: debuggingOptions.hostname,
port: debuggingOptions.port,
skipDwds: device is WebServerDevice,
skipDwds: device is WebServerDevice || !debuggingOptions.buildInfo.isDebug,
);
// When connecting to a browser, update the message with a seemsSlow notification
// to handle the case where we fail to connect.
......@@ -291,6 +293,21 @@ class ResidentWebRunner extends ResidentRunner {
).send();
}
}
// Allows browser refresh hot restart on non-debug builds.
if (device is ChromeDevice && debuggingOptions.browserLaunch) {
try {
final Chrome chrome = await ChromeLauncher.connectedInstance;
final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
return chromeTab.url.contains(debuggingOptions.hostname);
});
final WipConnection wipConnection = await chromeTab.connect();
await wipConnection.sendCommand('Page.reload');
status.stop();
return OperationResult.ok;
} catch (err) {
// Ignore error and continue with posted message;
}
}
status.stop();
printStatus('Recompile complete. Page requires refresh.');
return OperationResult.ok;
......
......@@ -35,6 +35,7 @@ import '../platform_plugins.dart';
import '../plugins.dart';
import '../project.dart';
import '../web/chrome.dart';
import '../web/compile.dart';
/// The name of the built web project.
const String kBuildTargetName = 'web';
......@@ -89,6 +90,11 @@ class WebFs {
this._dwds,
this.uri,
this._assetServer,
this._useBuildRunner,
this._flutterProject,
this._target,
this._buildInfo,
this._initializePlatform,
);
/// The server uri.
......@@ -98,12 +104,17 @@ class WebFs {
final Dwds _dwds;
final BuildDaemonClient _client;
final AssetServer _assetServer;
final bool _useBuildRunner;
final FlutterProject _flutterProject;
final String _target;
final BuildInfo _buildInfo;
final bool _initializePlatform;
StreamSubscription<void> _connectedApps;
static const String _kHostName = 'localhost';
Future<void> stop() async {
await _client.close();
await _client?.close();
await _dwds?.stop();
await _server.close(force: true);
await _connectedApps?.cancel();
......@@ -132,6 +143,10 @@ class WebFs {
/// Recompile the web application and return whether this was successful.
Future<bool> recompile() async {
if (!_useBuildRunner) {
await buildWeb(_flutterProject, _target, _buildInfo, _initializePlatform);
return true;
}
_client.startBuild();
await for (BuildResults results in _client.buildResults) {
final BuildResult result = results.results.firstWhere((BuildResult result) {
......@@ -161,39 +176,7 @@ class WebFs {
if (!flutterProject.dartTool.existsSync()) {
flutterProject.dartTool.createSync(recursive: true);
}
final bool hasWebPlugins = findPlugins(flutterProject)
.any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
// Start the build daemon and run an initial build.
final Completer<bool> inititalBuild = Completer<bool>();
final BuildDaemonClient client = await buildDaemonCreator
.startBuildDaemon(fs.currentDirectory.path,
release: buildInfo.isRelease,
profile: buildInfo.isProfile,
hasPlugins: hasWebPlugins,
initializePlatform: initializePlatform,
);
client.startBuild();
// Only provide relevant build results
final Stream<BuildResult> filteredBuildResults = client.buildResults
.asyncMap<BuildResult>((BuildResults results) {
return results.results
.firstWhere((BuildResult result) => result.target == kBuildTargetName);
});
final StreamSubscription<void> firstBuild = client.buildResults.listen((BuildResults buildResults) {
if (inititalBuild.isCompleted) {
return;
}
final BuildResult result = buildResults.results.firstWhere((BuildResult result) {
return result.target == kBuildTargetName;
});
if (result.status == BuildStatus.failed) {
inititalBuild.complete(false);
}
if (result.status == BuildStatus.succeeded) {
inititalBuild.complete(true);
}
});
final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory);
final Completer<bool> firstBuildCompleter = Completer<bool>();
// Initialize the asset bundle.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
......@@ -201,7 +184,7 @@ class WebFs {
await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
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>{
'main.dart.js': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.dart.js',
......@@ -236,29 +219,78 @@ class WebFs {
}
};
});
Handler handler;
Dwds dwds;
if (!skipDwds) {
dwds = await dwdsFactory(
hostname: hostname ?? _kHostName,
applicationPort: hostPort,
applicationTarget: kBuildTargetName,
assetServerPort: daemonAssetPort,
buildResults: filteredBuildResults,
chromeConnection: () async {
return (await ChromeLauncher.connectedInstance).chromeConnection;
},
reloadConfiguration: ReloadConfiguration.none,
serveDevTools: true,
verbose: false,
enableDebugExtension: true,
logWriter: (dynamic level, String message) => printTrace(message),
);
handler = pipeline.addHandler(dwds.handler);
BuildDaemonClient client;
StreamSubscription<void> firstBuild;
if (buildInfo.isDebug) {
final bool hasWebPlugins = findPlugins(flutterProject)
.any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
// Start the build daemon and run an initial build.
client = await buildDaemonCreator
.startBuildDaemon(fs.currentDirectory.path,
release: buildInfo.isRelease,
profile: buildInfo.isProfile,
hasPlugins: hasWebPlugins,
initializePlatform: initializePlatform,
);
client.startBuild();
// Only provide relevant build results
final Stream<BuildResult> filteredBuildResults = client.buildResults
.asyncMap<BuildResult>((BuildResults results) {
return results.results
.firstWhere((BuildResult result) => result.target == kBuildTargetName);
});
// Start the build daemon and run an initial build.
firstBuild = client.buildResults.listen((BuildResults buildResults) {
if (firstBuildCompleter.isCompleted) {
return;
}
final BuildResult result = buildResults.results.firstWhere((BuildResult result) {
return result.target == kBuildTargetName;
});
if (result.status == BuildStatus.failed) {
firstBuildCompleter.complete(false);
}
if (result.status == BuildStatus.succeeded) {
firstBuildCompleter.complete(true);
}
});
final int daemonAssetPort = buildDaemonCreator.assetServerPort(fs.currentDirectory);
// Initialize the asset bundle.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
await assetBundle.build();
await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
if (!skipDwds) {
dwds = await dwdsFactory(
hostname: hostname ?? _kHostName,
applicationPort: hostPort,
applicationTarget: kBuildTargetName,
assetServerPort: daemonAssetPort,
buildResults: filteredBuildResults,
chromeConnection: () async {
return (await ChromeLauncher.connectedInstance).chromeConnection;
},
reloadConfiguration: ReloadConfiguration.none,
serveDevTools: true,
verbose: false,
enableDebugExtension: true,
logWriter: (dynamic level, String message) => printTrace(message),
);
handler = pipeline.addHandler(dwds.handler);
} else {
handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
}
} else {
handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
await buildWeb(flutterProject, target, buildInfo, initializePlatform);
firstBuildCompleter.complete(true);
}
final AssetServer assetServer = AssetServer(flutterProject, targetBaseName);
final AssetServer assetServer = buildInfo.isDebug
? DebugAssetServer(flutterProject, targetBaseName)
: ReleaseAssetServer();
Cascade cascade = Cascade();
cascade = cascade.add(handler);
cascade = cascade.add(assetServer.handle);
......@@ -270,23 +302,65 @@ class WebFs {
dwds,
'http://$_kHostName:$hostPort/',
assetServer,
buildInfo.isDebug,
flutterProject,
target,
buildInfo,
initializePlatform,
);
if (!await inititalBuild.future) {
if (!await firstBuildCompleter.future) {
throw Exception('Failed to compile for the web.');
}
await firstBuild.cancel();
await firstBuild?.cancel();
return webFS;
}
}
class AssetServer {
AssetServer(this.flutterProject, this.targetBaseName);
abstract class AssetServer {
Future<Response> handle(Request request);
void dispose() {}
}
class ReleaseAssetServer extends AssetServer {
@override
Future<Response> handle(Request request) async {
final Uri artifactUri = fs.directory(getWebBuildDirectory()).uri.resolveUri(request.url);
final File file = fs.file(artifactUri);
if (file.existsSync()) {
return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': _guessExtension(file),
});
}
if (request.url.path == '') {
final File file = fs.file(fs.path.join(getWebBuildDirectory(), 'index.html'));
return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': _guessExtension(file),
});
}
return Response.notFound('');
}
String _guessExtension(File file) {
switch (fs.path.extension(file.path)) {
case '.js':
return 'text/javascript';
case '.html':
return 'text/html';
}
return 'text';
}
}
class DebugAssetServer extends AssetServer {
DebugAssetServer(this.flutterProject, this.targetBaseName);
final FlutterProject flutterProject;
final String targetBaseName;
final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath);
Directory partFiles;
@override
Future<Response> handle(Request request) async {
if (request.url.path.endsWith('.html')) {
final Uri htmlUri = flutterProject.web.directory.uri.resolveUri(request.url);
......@@ -411,6 +485,7 @@ class AssetServer {
return Response.notFound('');
}
@override
void dispose() {
partFiles?.deleteSync(recursive: true);
}
......
......@@ -25,6 +25,13 @@ export 'source.dart';
/// The [BuildSystem] instance.
BuildSystem get buildSystem => context.get<BuildSystem>();
/// A reasonable amount of files to open at the same time.
///
/// This number is somewhat arbitrary - it is difficult to detect whether
/// or not we'll run out of file descriptiors when using async dart:io
/// APIs.
const int kMaxOpenFiles = 64;
/// Configuration for the build system itself.
class BuildSystemConfig {
/// Create a new [BuildSystemConfig].
......
......@@ -7,6 +7,7 @@ import 'dart:collection';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:pool/pool.dart';
import '../base/file_system.dart';
import '../convert.dart';
......@@ -69,13 +70,6 @@ class FileHash {
///
/// The format of the file store is subject to change and not part of its API.
///
/// To regenerate the protobuf entries used to construct the cache:
/// 1. If not already installed, https://developers.google.com/protocol-buffers/docs/downloads
/// 2. pub global active `protoc-gen-dart`
/// 3. protoc -I=lib/src/build_system/ --dart_out=lib/src/build_system/ lib/src/build_system/filecache.proto
/// 4. Add licenses headers to the newly generated file and check-in.
///
/// See also: https://developers.google.com/protocol-buffers/docs/darttutorial
// TODO(jonahwilliams): find a better way to clear out old entries, perhaps
// track the last access or modification date?
class FileHashStore {
......@@ -141,24 +135,27 @@ class FileHashStore {
/// Computes a hash of the provided files and returns a list of entities
/// that were dirty.
// TODO(jonahwilliams): compare hash performance with md5 tool on macOS and
// linux and certutil on Windows, as well as dividing up computation across
// isolates. This also related to the current performance issue with checking
// APKs before installing them on device.
Future<List<File>> hashFiles(List<File> files) async {
final List<File> dirty = <File>[];
for (File file in files) {
final String absolutePath = file.resolveSymbolicLinksSync();
final String previousHash = previousHashes[absolutePath];
final List<int> bytes = file.readAsBytesSync();
final String currentHash = md5.convert(bytes).toString();
await Future.wait(<Future<void>>[
for (File file in files) _hashFile(file, dirty, Pool(kMaxOpenFiles))]);
return dirty;
}
Future<void> _hashFile(File file, List<File> dirty, Pool pool) async {
final PoolResource resource = await pool.request();
try {
final String absolutePath = file.path;
final String previousHash = previousHashes[absolutePath];
final Digest digest = md5.convert(await file.readAsBytes());
final String currentHash = digest.toString();
if (currentHash != previousHash) {
dirty.add(file);
}
currentHashes[absolutePath] = currentHash;
} finally {
resource.release();
}
return dirty;
}
File get _cacheFile => environment.buildDir.childFile(_kFileCache);
......
......@@ -50,7 +50,9 @@ class AssetBehavior extends SourceBehavior {
/// A specific asset behavior for building bundles.
class AssetOutputBehavior extends SourceBehavior {
const AssetOutputBehavior();
const AssetOutputBehavior([this._pathSuffix = '']);
final String _pathSuffix;
@override
List<File> inputs(Environment environment) {
......@@ -64,7 +66,7 @@ class AssetOutputBehavior extends SourceBehavior {
final List<File> results = <File>[];
final Iterable<DevFSFileContent> files = assetBundle.entries.values.whereType<DevFSFileContent>();
for (DevFSFileContent devFsContent in files) {
results.add(fs.file(devFsContent.file.path));
results.add(fs.file(fs.path.join(_pathSuffix, devFsContent.file.path)));
}
return results;
}
......@@ -78,7 +80,7 @@ class AssetOutputBehavior extends SourceBehavior {
);
final List<File> results = <File>[];
for (String key in assetBundle.entries.keys) {
final File file = fs.file(fs.path.join(environment.outputDir.path, key));
final File file = fs.file(fs.path.join(environment.outputDir.path, _pathSuffix, key));
results.add(file);
}
return results;
......@@ -125,7 +127,7 @@ class CopyAssets extends Target {
packagesPath: environment.projectDir.childFile('.packages').path,
);
// Limit number of open files to avoid running out of file descriptors.
final Pool pool = Pool(64);
final Pool pool = Pool(kMaxOpenFiles);
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
......
......@@ -115,23 +115,7 @@ class CopyFlutterBundle extends Target {
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
await assetBundle.build();
final Pool pool = Pool(64);
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
final File file = fs.file(fs.path.join(environment.outputDir.path, entry.key));
file.parent.createSync(recursive: true);
final DevFSContent content = entry.value;
if (content is DevFSFileContent && content.file is File) {
await (content.file as File).copy(file.path);
} else {
await file.writeAsBytes(await entry.value.contentsAsBytes());
}
} finally {
resource.release();
}
}));
await copyAssets(assetBundle, environment);
}
@override
......@@ -140,6 +124,28 @@ class CopyFlutterBundle extends Target {
];
}
/// A helper function to copy an [assetBundle] into an [environment]'s output directory,
/// plus an optional [pathSuffix]
Future<void> copyAssets(AssetBundle assetBundle, Environment environment, [String pathSuffix = '']) async {
final Pool pool = Pool(kMaxOpenFiles);
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
final File file = fs.file(fs.path.join(environment.outputDir.path, pathSuffix, entry.key));
file.parent.createSync(recursive: true);
final DevFSContent content = entry.value;
if (content is DevFSFileContent && content.file is File) {
await (content.file as File).copy(file.path);
} else {
await file.writeAsBytes(await entry.value.contentsAsBytes());
}
} finally {
resource.release();
}
}));
}
/// Copies the prebuilt flutter bundle for release mode.
class ReleaseCopyFlutterBundle extends CopyFlutterBundle {
const ReleaseCopyFlutterBundle();
......
......@@ -336,7 +336,7 @@ abstract class MacOSBundleFlutterAssets extends Target {
}
// Limit number of open files to avoid running out of file descriptors.
try {
final Pool pool = Pool(64);
final Pool pool = Pool(kMaxOpenFiles);
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
......
// 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 '../../artifacts.dart';
import '../../asset.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/process_manager.dart';
import '../../build_info.dart';
import '../../dart/package_map.dart';
import '../../globals.dart';
import '../../project.dart';
import '../build_system.dart';
import 'assets.dart';
import 'dart.dart';
/// Whether web builds should call the platform initialization logic.
const String kInitializePlatform = 'InitializePlatform';
/// Whether the application has web plugins.
const String kHasWebPlugins = 'HasWebPlugins';
/// An override for the dart2js build mode.
///
/// Valid values are O1 (lowest, profile default) to O4 (highest, release default).
const String kDart2jsOptimization = 'Dart2jsOptimization';
/// Generates an entrypoint for a web target.
class WebEntrypointTarget extends Target {
const WebEntrypointTarget();
@override
String get name => 'web_entrypoint';
@override
List<Target> get dependencies => const <Target>[];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/main.dart'),
];
@override
Future<void> build(Environment environment) async {
final String targetFile = environment.defines[kTargetFile];
final bool shouldInitializePlatform = environment.defines[kInitializePlatform] == 'true';
final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true';
final String import = fs.file(fs.path.absolute(targetFile)).uri.toString();
String contents;
if (hasPlugins) {
contents = '''
import 'dart:ui' as ui;
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'generated_plugin_registrant.dart';
import "$import" as entrypoint;
Future<void> main() async {
registerPlugins(webPluginRegistry);
if ($shouldInitializePlatform) {
await ui.webOnlyInitializePlatform();
}
entrypoint.main();
}
''';
} else {
contents = '''
import 'dart:ui' as ui;
import "$import" as entrypoint;
Future<void> main() async {
if ($shouldInitializePlatform) {
await ui.webOnlyInitializePlatform();
}
entrypoint.main();
}
''';
}
environment.buildDir.childFile('main.dart')
..writeAsStringSync(contents);
}
}
/// Compiles a web entrypoint with dart2js.
class Dart2JSTarget extends Target {
const Dart2JSTarget();
@override
String get name => 'dart2js';
@override
List<Target> get dependencies => const <Target>[
WebEntrypointTarget()
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
Source.artifact(Artifact.flutterWebSdk),
Source.artifact(Artifact.dart2jsSnapshot),
Source.artifact(Artifact.engineDartBinary),
Source.artifact(Artifact.engineDartSdkPath),
Source.pattern('{BUILD_DIR}/main.dart'),
Source.pattern('{PROJECT_DIR}/.packages'),
Source.function(listDartSources), // <- every dart file under {PROJECT_DIR}/lib and in .packages
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/main.dart.js'),
];
@override
Future<void> build(Environment environment) async {
final String dart2jsOptimization = environment.defines[kDart2jsOptimization];
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final String specPath = fs.path.join(artifacts.getArtifactPath(Artifact.flutterWebSdk), 'libraries.json');
final String packageFile = FlutterProject.fromDirectory(environment.projectDir).hasBuilders
? PackageMap.globalGeneratedPackagesPath
: PackageMap.globalPackagesPath;
final ProcessResult result = await processManager.run(<String>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
artifacts.getArtifactPath(Artifact.dart2jsSnapshot),
'--libraries-spec=$specPath',
if (dart2jsOptimization != null)
'-$dart2jsOptimization'
else if (buildMode == BuildMode.profile)
'-O1'
else
'-O4',
'-o',
environment.buildDir.childFile('main.dart.js').path,
'--packages=$packageFile',
if (buildMode == BuildMode.profile)
'-Ddart.vm.profile=true'
else
'-Ddart.vm.product=true',
environment.buildDir.childFile('main.dart').path,
]);
if (result.exitCode != 0) {
throw Exception(result.stdout + result.stderr);
}
}
}
/// Unpacks the dart2js compilation to a given output directory
class WebReleaseBundle extends Target {
const WebReleaseBundle();
@override
String get name => 'web_release_bundle';
@override
List<Target> get dependencies => const <Target>[
Dart2JSTarget(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/main.dart.js'),
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
Source.behavior(AssetOutputBehavior('assets')),
Source.pattern('{PROJECT_DIR}/web/index.html'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/main.dart.js'),
Source.pattern('{OUTPUT_DIR}/assets/AssetManifest.json'),
Source.pattern('{OUTPUT_DIR}/assets/FontManifest.json'),
Source.pattern('{OUTPUT_DIR}/assets/LICENSE'),
Source.pattern('{OUTPUT_DIR}/index.html'),
Source.behavior(AssetOutputBehavior('assets'))
];
@override
Future<void> build(Environment environment) async {
for (File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
if (!fs.path.basename(outputFile.path).contains('main.dart.js')) {
continue;
}
outputFile.copySync(
environment.outputDir.childFile(fs.path.basename(outputFile.path)).path
);
}
environment.projectDir
.childDirectory('web')
.childFile('index.html')
.copySync(fs.path.join(environment.outputDir.path, 'index.html'));
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
await assetBundle.build();
await copyAssets(assetBundle, environment, 'assets');
}
}
......@@ -12,6 +12,7 @@ import '../build_system/targets/dart.dart';
import '../build_system/targets/ios.dart';
import '../build_system/targets/linux.dart';
import '../build_system/targets/macos.dart';
import '../build_system/targets/web.dart';
import '../build_system/targets/windows.dart';
import '../globals.dart';
import '../project.dart';
......@@ -31,6 +32,7 @@ const List<Target> _kDefaultTargets = <Target>[
DebugMacOSBundleFlutterAssets(),
ProfileMacOSBundleFlutterAssets(),
ReleaseMacOSBundleFlutterAssets(),
WebReleaseBundle(),
];
/// Assemble provides a low level API to interact with the flutter tool build
......
......@@ -2,17 +2,19 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:archive/archive.dart';
import 'package:meta/meta.dart';
import '../asset.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../build_system/build_system.dart';
import '../build_system/targets/dart.dart';
import '../build_system/targets/web.dart';
import '../globals.dart';
import '../platform_plugins.dart';
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
......@@ -23,64 +25,32 @@ Future<void> buildWeb(FlutterProject flutterProject, String target, BuildInfo bu
if (!flutterProject.web.existsSync()) {
throwToolExit('Missing index.html.');
}
final bool hasWebPlugins = findPlugins(flutterProject)
.any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null);
final Stopwatch sw = Stopwatch()..start();
final Directory outputDir = fs.directory(getWebBuildDirectory())
..createSync(recursive: true);
bool result;
try {
result = await webCompilationProxy.initialize(
projectDirectory: FlutterProject.current().directory,
mode: buildInfo.mode,
projectName: flutterProject.manifest.appName,
initializePlatform: initializePlatform,
);
if (result) {
// Places assets adjacent to the web stuff.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
await assetBundle.build();
await writeBundle(fs.directory(fs.path.join(outputDir.path, 'assets')), assetBundle.entries);
// Copy results to output directory.
final String outputPath = fs.path.join(
flutterProject.dartTool.path,
'build',
'flutter_web',
flutterProject.manifest.appName,
'${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.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map'));
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);
}
}
final BuildResult result = await const BuildSystem().build(const WebReleaseBundle(), Environment(
outputDir: fs.directory(getWebBuildDirectory()),
projectDir: fs.currentDirectory,
buildDir: flutterProject.directory
.childDirectory('.dart_tool')
.childDirectory('flutter_build'),
defines: <String, String>{
kBuildMode: getNameForBuildMode(buildInfo.mode),
kTargetFile: target,
kInitializePlatform: initializePlatform.toString(),
kHasWebPlugins: hasWebPlugins.toString(),
},
));
if (!result.success) {
for (ExceptionMeasurement measurement in result.exceptions.values) {
printError(measurement.stackTrace.toString());
printError(measurement.exception.toString());
}
} catch (err) {
printError(err.toString());
result = false;
} finally {
status.stop();
}
if (result == false) {
throwToolExit('Failed to compile $target for the Web.');
}
String buildName = 'ddc';
if (buildInfo.isRelease) {
buildName = 'dart2js';
throwToolExit('Failed to compile application for the Web.');
}
flutterUsage.sendTiming('build', buildName, Duration(milliseconds: sw.elapsedMilliseconds));
status.stop();
flutterUsage.sendTiming('build', 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds));
}
/// An indirection on web compilation.
......
......@@ -39,37 +39,37 @@ void main() {
expect(fileStorage.version, 2);
}));
test('saves and restores to file cache', () => testbed.run(() {
test('saves and restores to file cache', () => testbed.run(() async {
final File file = fs.file('foo.dart')
..createSync()
..writeAsStringSync('hello');
final FileHashStore fileCache = FileHashStore(environment);
fileCache.initialize();
fileCache.hashFiles(<File>[file]);
await fileCache.hashFiles(<File>[file]);
fileCache.persist();
final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()];
final String currentHash = fileCache.currentHashes[file.path];
final List<int> buffer = fs.file(fs.path.join(environment.buildDir.path, '.filecache'))
.readAsBytesSync();
FileStorage fileStorage = FileStorage.fromBuffer(buffer);
expect(fileStorage.files.single.hash, currentHash);
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
expect(fileStorage.files.single.path, file.path);
final FileHashStore newFileCache = FileHashStore(environment);
newFileCache.initialize();
expect(newFileCache.currentHashes, isEmpty);
expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')], currentHash);
expect(newFileCache.previousHashes['foo.dart'], currentHash);
newFileCache.persist();
// Still persisted correctly.
fileStorage = FileStorage.fromBuffer(buffer);
expect(fileStorage.files.single.hash, currentHash);
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
expect(fileStorage.files.single.path, file.path);
}));
test('handles persisting with a missing build directory', () => testbed.run(() {
test('handles persisting with a missing build directory', () => testbed.run(() async {
final File file = fs.file('foo.dart')
..createSync()
..writeAsStringSync('hello');
......@@ -77,7 +77,7 @@ void main() {
fileCache.initialize();
environment.buildDir.deleteSync(recursive: true);
fileCache.hashFiles(<File>[file]);
await fileCache.hashFiles(<File>[file]);
// Does not throw.
fileCache.persist();
}));
......
// 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/base/process_manager.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/build_system/targets/web.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../../src/common.dart';
import '../../../src/mocks.dart';
import '../../../src/testbed.dart';
void main() {
Testbed testbed;
Environment environment;
MockPlatform mockPlatform;
MockPlatform mockWindowsPlatform;
setUp(() {
mockPlatform = MockPlatform();
mockWindowsPlatform = MockPlatform();
when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(true);
when(mockPlatform.isLinux).thenReturn(false);
when(mockWindowsPlatform.isWindows).thenReturn(true);
when(mockWindowsPlatform.isMacOS).thenReturn(false);
when(mockWindowsPlatform.isLinux).thenReturn(false);
testbed = Testbed(setup: () {
environment = Environment(
projectDir: fs.currentDirectory,
outputDir: fs.currentDirectory,
buildDir: fs.currentDirectory,
defines: <String, String>{
kTargetFile: fs.path.join('lib', 'main.dart'),
}
);
environment.buildDir.createSync(recursive: true);
}, overrides: <Type, Generator>{
Platform: () => mockPlatform,
});
});
test('WebEntrypointTarget generates an entrypoint with plugins and init platform', () => testbed.run(() async {
environment.defines[kHasWebPlugins] = 'true';
environment.defines[kInitializePlatform] = 'true';
await const WebEntrypointTarget().build(environment);
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Plugins
expect(generated, contains("import 'generated_plugin_registrant.dart';"));
expect(generated, contains('registerPlugins(webPluginRegistry);'));
// Platform
expect(generated, contains('if (true) {'));
// Main
expect(generated, contains('entrypoint.main();'));
// Import.
expect(generated, contains('import "file:///lib/main.dart" as entrypoint;'));
}));
test('WebEntrypointTarget generates an entrypoint with plugins and init platform on windows', () => testbed.run(() async {
environment.defines[kHasWebPlugins] = 'true';
environment.defines[kInitializePlatform] = 'true';
await const WebEntrypointTarget().build(environment);
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Plugins
expect(generated, contains("import 'generated_plugin_registrant.dart';"));
expect(generated, contains('registerPlugins(webPluginRegistry);'));
// Platform
expect(generated, contains('if (true) {'));
// Main
expect(generated, contains('entrypoint.main();'));
// Import.
expect(generated, contains('import "file:///C:/lib/main.dart" as entrypoint;'));
}, overrides: <Type, Generator>{
Platform: () => mockWindowsPlatform,
}));
test('WebEntrypointTarget generates an entrypoint without plugins and init platform', () => testbed.run(() async {
environment.defines[kHasWebPlugins] = 'false';
environment.defines[kInitializePlatform] = 'true';
await const WebEntrypointTarget().build(environment);
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Plugins
expect(generated, isNot(contains("import 'generated_plugin_registrant.dart';")));
expect(generated, isNot(contains('registerPlugins(webPluginRegistry);')));
// Platform
expect(generated, contains('if (true) {'));
// Main
expect(generated, contains('entrypoint.main();'));
}));
test('WebEntrypointTarget generates an entrypoint with plugins and without init platform', () => testbed.run(() async {
environment.defines[kHasWebPlugins] = 'true';
environment.defines[kInitializePlatform] = 'false';
await const WebEntrypointTarget().build(environment);
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Plugins
expect(generated, contains("import 'generated_plugin_registrant.dart';"));
expect(generated, contains('registerPlugins(webPluginRegistry);'));
// Platform
expect(generated, contains('if (false) {'));
// Main
expect(generated, contains('entrypoint.main();'));
}));
test('WebEntrypointTarget generates an entrypoint without plugins and without init platform', () => testbed.run(() async {
environment.defines[kHasWebPlugins] = 'false';
environment.defines[kInitializePlatform] = 'false';
await const WebEntrypointTarget().build(environment);
final String generated = environment.buildDir.childFile('main.dart').readAsStringSync();
// Plugins
expect(generated, isNot(contains("import 'generated_plugin_registrant.dart';")));
expect(generated, isNot(contains('registerPlugins(webPluginRegistry);')));
// Platform
expect(generated, contains('if (false) {'));
// Main
expect(generated, contains('entrypoint.main();'));
}));
test('Dart2JSTarget calls dart2js with expected args in profile mode', () => testbed.run(() async {
environment.defines[kBuildMode] = 'profile';
when(processManager.run(any)).thenAnswer((Invocation invocation) async {
return FakeProcessResult(exitCode: 0);
});
await const Dart2JSTarget().build(environment);
final List<String> expected = <String>[
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
'--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
'-O1', // lowest optimizations.
'-o',
environment.buildDir.childFile('main.dart.js').absolute.path,
'--packages=.packages',
'-Ddart.vm.profile=true',
environment.buildDir.childFile('main.dart').absolute.path,
];
verify(processManager.run(expected)).called(1);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
}));
test('Dart2JSTarget calls dart2js with expected args in release mode', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release';
when(processManager.run(any)).thenAnswer((Invocation invocation) async {
return FakeProcessResult(exitCode: 0);
});
await const Dart2JSTarget().build(environment);
final List<String> expected = <String>[
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
'--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
'-O4', // highest optimizations.
'-o',
environment.buildDir.childFile('main.dart.js').absolute.path,
'--packages=.packages',
'-Ddart.vm.product=true',
environment.buildDir.childFile('main.dart').absolute.path,
];
verify(processManager.run(expected)).called(1);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
}));
test('Dart2JSTarget calls dart2js with expected args in release with dart2js optimization override', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release';
environment.defines[kDart2jsOptimization] = 'O3';
when(processManager.run(any)).thenAnswer((Invocation invocation) async {
return FakeProcessResult(exitCode: 0);
});
await const Dart2JSTarget().build(environment);
final List<String> expected = <String>[
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'snapshots', 'dart2js.dart.snapshot'),
'--libraries-spec=' + fs.path.join('bin', 'cache', 'flutter_web_sdk', 'libraries.json'),
'-O3', // configured optimizations.
'-o',
environment.buildDir.childFile('main.dart.js').absolute.path,
'--packages=.packages',
'-Ddart.vm.product=true',
environment.buildDir.childFile('main.dart').absolute.path,
];
verify(processManager.run(expected)).called(1);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
}));
}
class MockProcessManager extends Mock implements ProcessManager {}
class MockPlatform extends Mock implements Platform {}
......@@ -2,11 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:archive/archive.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/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
......@@ -26,10 +22,8 @@ import '../../src/common.dart';
import '../../src/testbed.dart';
void main() {
MockWebCompilationProxy mockWebCompilationProxy;
Testbed testbed;
MockPlatform mockPlatform;
bool addArchive = false;
setUpAll(() {
Cache.flutterRoot = '';
......@@ -37,8 +31,6 @@ void main() {
});
setUp(() {
addArchive = false;
mockWebCompilationProxy = MockWebCompilationProxy();
testbed = Testbed(setup: () {
fs.file('pubspec.yaml')
..createSync()
......@@ -46,47 +38,13 @@ void main() {
fs.file('.packages').createSync();
fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true);
fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
when(mockWebCompilationProxy.initialize(
projectName: anyNamed('projectName'),
projectDirectory: anyNamed('projectDirectory'),
mode: anyNamed('mode'),
initializePlatform: anyNamed('initializePlatform'),
)).thenAnswer((Invocation invocation) {
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.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);
});
}, overrides: <Type, Generator>{
WebCompilationProxy: () => mockWebCompilationProxy,
Platform: () => mockPlatform,
FlutterVersion: () => MockFlutterVersion(),
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
});
});
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 {
fs.file(fs.path.join('web', 'index.html')).deleteSync();
......
......@@ -22,7 +22,7 @@ void main() {
fs.file(fs.path.join('web', 'index.html'))
..createSync(recursive: true)
..writeAsStringSync('hello');
assetServer = AssetServer(FlutterProject.current(), fs.path.join('main'));
assetServer = DebugAssetServer(FlutterProject.current(), fs.path.join('main'));
}
);
});
......
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