diff --git a/dev/benchmarks/macrobenchmarks/web/index.html b/dev/benchmarks/macrobenchmarks/web/index.html index dec5a5150c..f73aee4308 100644 --- a/dev/benchmarks/macrobenchmarks/web/index.html +++ b/dev/benchmarks/macrobenchmarks/web/index.html @@ -6,8 +6,12 @@ found in the LICENSE file. --> Web Benchmarks + - + diff --git a/dev/devicelab/lib/tasks/web_benchmarks.dart b/dev/devicelab/lib/tasks/web_benchmarks.dart index a4748b8f9f..031d478fcf 100644 --- a/dev/devicelab/lib/tasks/web_benchmarks.dart +++ b/dev/devicelab/lib/tasks/web_benchmarks.dart @@ -39,7 +39,7 @@ Future runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async { '--omit-type-checks', ], '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true', - '--web-renderer=${benchmarkOptions.webRenderer}', + if (!benchmarkOptions.useWasm) '--web-renderer=${benchmarkOptions.webRenderer}', '--profile', '--no-web-resources-cdn', '-t', @@ -125,7 +125,7 @@ Future runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async { return Response.internalServerError(body: '$error'); } }).add(createBuildDirectoryHandler( - path.join(macrobenchmarksDirectory, 'build', benchmarkOptions.useWasm ? 'web_wasm' : 'web'), + path.join(macrobenchmarksDirectory, 'build', 'web'), )); server = await io.HttpServer.bind('localhost', benchmarkServerPort); diff --git a/dev/integration_tests/web_e2e_tests/web/index.html b/dev/integration_tests/web_e2e_tests/web/index.html index 9dcd43f6d1..70d8bac291 100644 --- a/dev/integration_tests/web_e2e_tests/web/index.html +++ b/dev/integration_tests/web_e2e_tests/web/index.html @@ -5,14 +5,17 @@ found in the LICENSE file. --> Web Integration Tests - + - + diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 6b107e10ab..59e2459262 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -12,7 +12,6 @@ import 'base/os.dart'; import 'base/utils.dart'; import 'convert.dart'; import 'globals.dart' as globals; -import 'web/compile.dart'; /// Whether icon font subsetting is enabled by default. const bool kIconTreeShakerEnabledDefault = true; @@ -36,7 +35,6 @@ class BuildInfo { List? dartDefines, this.bundleSkSLPath, List? dartExperiments, - this.webRenderer = WebRendererMode.auto, required this.treeShakeIcons, this.performanceMeasurementFile, this.packagesPath = '.dart_tool/package_config.json', // TODO(zanderso): make this required and remove the default. @@ -130,9 +128,6 @@ class BuildInfo { /// A list of Dart experiments. final List dartExperiments; - /// When compiling to web, which web renderer mode we are using (html, canvaskit, auto) - final WebRendererMode webRenderer; - /// The name of a file where flutter assemble will output performance /// information in a JSON format. /// @@ -798,10 +793,6 @@ HostPlatform getCurrentHostPlatform() { return HostPlatform.linux_x64; } -FileSystemEntity getWebPlatformBinariesDirectory(Artifacts artifacts, WebRendererMode webRenderer) { - return artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder); -} - /// Returns the top-level build output directory. String getBuildDirectory([Config? config, FileSystem? fileSystem]) { // TODO(johnmccutchan): Stop calling this function as part of setting @@ -844,8 +835,8 @@ String getMacOSBuildDirectory() { } /// Returns the web build output directory. -String getWebBuildDirectory([bool isWasm = false]) { - return globals.fs.path.join(getBuildDirectory(), isWasm ? 'web_wasm' : 'web'); +String getWebBuildDirectory() { + return globals.fs.path.join(getBuildDirectory(), 'web'); } /// Returns the Linux build output directory. diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index 2438202015..849f4f3ce0 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -136,6 +136,15 @@ abstract class Target { /// A list of zero or more depfiles, located directly under {BUILD_DIR}. List get depfiles => const []; + /// A string that differentiates different build variants from each other + /// with regards to build flags or settings on the target. This string should + /// represent each build variant as a different unique value. If this value + /// changes between builds, the target will be invalidated and rebuilt. + /// + /// By default, this returns null, which indicates there is only one build + /// variant, and the target won't invalidate or rebuild due to this property. + String? get buildKey => null; + /// Whether this target can be executed with the given [environment]. /// /// Returning `true` will cause [build] to be skipped. This is equivalent @@ -156,6 +165,7 @@ abstract class Target { [ for (final Target target in dependencies) target._toNode(environment), ], + buildKey, environment, inputsFiles.containsNewDepfile, ); @@ -181,9 +191,11 @@ abstract class Target { for (final File output in outputs) { outputPaths.add(output.path); } + final String? key = buildKey; final Map result = { 'inputs': inputPaths, 'outputs': outputPaths, + if (key != null) 'buildKey': key, }; if (!stamp.existsSync()) { stamp.createSync(); @@ -218,6 +230,7 @@ abstract class Target { /// This requires constants from the [Environment] to resolve the paths of /// inputs and the output stamp. Map toJson(Environment environment) { + final String? key = buildKey; return { 'name': name, 'dependencies': [ @@ -229,6 +242,7 @@ abstract class Target { 'outputs': [ for (final File file in resolveOutputs(environment).sources) file.path, ], + if (key != null) 'buildKey': key, 'stamp': _findStampFile(environment).absolute.path, }; } @@ -980,49 +994,85 @@ void verifyOutputDirectories(List outputs, Environment environment, Target /// A node in the build graph. class Node { - Node( + factory Node( + Target target, + List inputs, + List outputs, + List dependencies, + String? buildKey, + Environment environment, + bool missingDepfile, + ) { + final File stamp = target._findStampFile(environment); + Map? stampValues; + + // If the stamp file doesn't exist, we haven't run this step before and + // all inputs were added. + if (stamp.existsSync()) { + final String content = stamp.readAsStringSync(); + if (content.isEmpty) { + stamp.deleteSync(); + } else { + try { + stampValues = castStringKeyedMap(json.decode(content)); + } on FormatException { + // The json is malformed in some way. + } + } + } + if (stampValues != null) { + final String? previousBuildKey = stampValues['buildKey'] as String?; + final Object? stampInputs = stampValues['inputs']; + final Object? stampOutputs = stampValues['outputs']; + if (stampInputs is List && stampOutputs is List) { + final Set previousInputs = stampInputs.whereType().toSet(); + final Set previousOutputs = stampOutputs.whereType().toSet(); + return Node.withStamp( + target, + inputs, + previousInputs, + outputs, + previousOutputs, + dependencies, + buildKey, + previousBuildKey, + missingDepfile, + ); + } + } + return Node.withNoStamp( + target, + inputs, + outputs, + dependencies, + buildKey, + missingDepfile, + ); + } + + Node.withNoStamp( this.target, this.inputs, this.outputs, this.dependencies, - Environment environment, + this.buildKey, this.missingDepfile, - ) { - final File stamp = target._findStampFile(environment); + ) : previousInputs = {}, + previousOutputs = {}, + previousBuildKey = null, + _dirty = true; - // If the stamp file doesn't exist, we haven't run this step before and - // all inputs were added. - if (!stamp.existsSync()) { - // No stamp file, not safe to skip. - _dirty = true; - return; - } - final String content = stamp.readAsStringSync(); - // Something went wrong writing the stamp file. - if (content.isEmpty) { - stamp.deleteSync(); - // Malformed stamp file, not safe to skip. - _dirty = true; - return; - } - Map? values; - try { - values = castStringKeyedMap(json.decode(content)); - } on FormatException { - // The json is malformed in some way. - _dirty = true; - return; - } - final Object? inputs = values?['inputs']; - final Object? outputs = values?['outputs']; - if (inputs is List && outputs is List) { - inputs.cast().whereType().forEach(previousInputs.add); - outputs.cast().whereType().forEach(previousOutputs.add); - } else { - // The json is malformed in some way. - _dirty = true; - } - } + Node.withStamp( + this.target, + this.inputs, + this.previousInputs, + this.outputs, + this.previousOutputs, + this.dependencies, + this.buildKey, + this.previousBuildKey, + this.missingDepfile, + ) : _dirty = false; /// The resolved input files. /// @@ -1034,6 +1084,11 @@ class Node { /// These files may not yet exist if the target hasn't run yet. final List outputs; + /// The current build key of the target + /// + /// See `buildKey` in the `Target` class for more information. + final String? buildKey; + /// Whether this node is missing a depfile. /// /// This requires an additional pass of source resolution after the target @@ -1047,10 +1102,15 @@ class Node { final List dependencies; /// Output file paths from the previous invocation of this build node. - final Set previousOutputs = {}; + final Set previousOutputs; /// Input file paths from the previous invocation of this build node. - final Set previousInputs = {}; + final Set previousInputs; + + /// The buildKey from the previous invocation of this build node. + /// + /// See `buildKey` in the `Target` class for more information. + final String? previousBuildKey; /// One or more reasons why a task was invalidated. /// @@ -1074,6 +1134,10 @@ class Node { FileSystem fileSystem, Logger logger, ) { + if (buildKey != previousBuildKey) { + _invalidate(InvalidatedReasonKind.buildKeyChanged); + _dirty = true; + } final Set currentOutputPaths = { for (final File file in outputs) file.path, }; @@ -1173,7 +1237,8 @@ class InvalidatedReason { InvalidatedReasonKind.inputChanged => 'The following inputs have updated contents: ${data.join(',')}', InvalidatedReasonKind.outputChanged => 'The following outputs have updated contents: ${data.join(',')}', InvalidatedReasonKind.outputMissing => 'The following outputs were missing: ${data.join(',')}', - InvalidatedReasonKind.outputSetChanged => 'The following outputs were removed from the output set: ${data.join(',')}' + InvalidatedReasonKind.outputSetChanged => 'The following outputs were removed from the output set: ${data.join(',')}', + InvalidatedReasonKind.buildKeyChanged => 'The target build key changed.', }; } } @@ -1195,4 +1260,7 @@ enum InvalidatedReasonKind { /// The set of expected output files changed. outputSetChanged, + + /// The build key changed + buildKeyChanged, } diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 28413c44d6..7b5035c2f1 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:crypto/crypto.dart'; +import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import '../../artifacts.dart'; @@ -22,7 +23,6 @@ import '../../project.dart'; import '../../web/compile.dart'; import '../../web/file_generators/flutter_service_worker_js.dart'; import '../../web/file_generators/main_dart.dart' as main_dart; -import '../../web/file_generators/wasm_bootstrap.dart' as wasm_bootstrap; import '../build_system.dart'; import '../depfile.dart'; import '../exceptions.dart'; @@ -38,6 +38,17 @@ const String kBaseHref = 'baseHref'; /// The caching strategy to use for service worker generation. const String kServiceWorkerStrategy = 'ServiceWorkerStrategy'; +@visibleForTesting +List updateDartDefines(List dartDefines, WebRendererMode webRenderer) { + final Set dartDefinesSet = dartDefines.toSet(); + if (!dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_AUTO_DETECT=')) + && dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA='))) { + dartDefinesSet.removeWhere((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA=')); + } + dartDefinesSet.addAll(webRenderer.dartDefines); + return dartDefinesSet.toList(); +} + /// Generates an entry point for a web target. // Keep this in sync with build_runner/resident_web_runner.dart class WebEntrypointTarget extends Target { @@ -104,11 +115,15 @@ class WebEntrypointTarget extends Target { /// Compiles a web entry point with dart2js. abstract class Dart2WebTarget extends Target { - const Dart2WebTarget(this.webRenderer); + const Dart2WebTarget(); - final WebRendererMode webRenderer; Source get compilerSnapshot; + WebCompilerConfig get compilerConfig; + + Map get buildConfig; + List get buildFiles; + @override List get dependencies => const [ WebEntrypointTarget(), @@ -125,11 +140,19 @@ abstract class Dart2WebTarget extends Target { ]; @override - List get outputs => const []; + List get outputs => buildFiles.map( + (String file) => Source.pattern('{OUTPUT_DIR}/$file') + ).toList(); + + @override + String get buildKey => compilerConfig.buildKey; } class Dart2JSTarget extends Dart2WebTarget { - Dart2JSTarget(super.webRenderer); + Dart2JSTarget(this.compilerConfig); + + @override + final JsCompilerConfig compilerConfig; @override String get name => 'dart2js'; @@ -149,9 +172,12 @@ class Dart2JSTarget extends Dart2WebTarget { throw MissingDefineException(kBuildMode, name); } final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); - final JsCompilerConfig compilerConfig = JsCompilerConfig.fromBuildSystemEnvironment(environment.defines); final Artifacts artifacts = environment.artifacts; - final String platformBinariesPath = getWebPlatformBinariesDirectory(artifacts, webRenderer).path; + final String platformBinariesPath = artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path; + final List dartDefines = updateDartDefines( + decodeDartDefines(environment.defines, kDartDefines), + compilerConfig.renderer, + ); final List sharedCommandOptions = [ artifacts.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript), '--disable-dart-dev', @@ -163,7 +189,7 @@ class Dart2JSTarget extends Dart2WebTarget { '-Ddart.vm.profile=true' else '-Ddart.vm.product=true', - for (final String dartDefine in decodeDartDefines(environment.defines, kDartDefines)) + for (final String dartDefine in dartDefines) '-D$dartDefine', ]; @@ -217,10 +243,25 @@ class Dart2JSTarget extends Dart2WebTarget { environment.buildDir.childFile('dart2js.d'), ); } + + @override + Map get buildConfig => { + 'compileTarget': 'dart2js', + 'renderer': compilerConfig.renderer.name, + 'mainJsPath': 'main.dart.js', + }; + + @override + List get buildFiles => [ + 'main.dart.js', + ]; } class Dart2WasmTarget extends Dart2WebTarget { - Dart2WasmTarget(super.webRenderer); + Dart2WasmTarget(this.compilerConfig); + + @override + final WasmCompilerConfig compilerConfig; @override Future build(Environment environment) async { @@ -228,7 +269,6 @@ class Dart2WasmTarget extends Dart2WebTarget { if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, name); } - final WasmCompilerConfig compilerConfig = WasmCompilerConfig.fromBuildSystemEnvironment(environment.defines); final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final Artifacts artifacts = environment.artifacts; final File outputWasmFile = environment.buildDir.childFile( @@ -236,8 +276,12 @@ class Dart2WasmTarget extends Dart2WebTarget { ); final File depFile = environment.buildDir.childFile('dart2wasm.d'); final String dartSdkPath = artifacts.getArtifactPath(Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript); - final String platformBinariesPath = getWebPlatformBinariesDirectory(artifacts, webRenderer).path; + final String platformBinariesPath = artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path; final String platformFilePath = environment.fileSystem.path.join(platformBinariesPath, 'dart2wasm_platform.dill'); + final List dartDefines = updateDartDefines( + decodeDartDefines(environment.defines, kDartDefines), + compilerConfig.renderer, + ); final List compilationArgs = [ artifacts.getArtifactPath(Artifact.engineDartAotRuntime, platform: TargetPlatform.web_javascript), @@ -251,10 +295,10 @@ class Dart2WasmTarget extends Dart2WebTarget { else '-Ddart.vm.product=true', ...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions), - for (final String dartDefine in decodeDartDefines(environment.defines, kDartDefines)) + for (final String dartDefine in dartDefines) '-D$dartDefine', ...compilerConfig.toCommandOptions(), - if (webRenderer == WebRendererMode.skwasm) + if (compilerConfig.renderer == WebRendererMode.skwasm) ...[ '--import-shared-memory', '--shared-memory-max-pages=32768', @@ -319,74 +363,75 @@ class Dart2WasmTarget extends Dart2WebTarget { ]; @override - List get outputs => const [ - Source.pattern('{OUTPUT_DIR}/main.dart.wasm'), - Source.pattern('{OUTPUT_DIR}/main.dart.mjs'), + List get outputs => const []; + + @override + Map get buildConfig => { + 'compileTarget': 'dart2wasm', + 'renderer': compilerConfig.renderer.name, + 'mainWasmPath': 'main.dart.wasm', + 'jsSupportRuntimePath': 'main.dart.mjs', + }; + + @override + List get buildFiles => [ + 'main.dart.wasm', + 'main.dart.mjs', ]; } /// Unpacks the dart2js or dart2wasm compilation and resources to a given /// output directory. class WebReleaseBundle extends Target { - const WebReleaseBundle(this.webRenderer, {required this.isWasm}); + WebReleaseBundle(List configs) : this._withTargets( + configs.map((WebCompilerConfig config) => + switch (config) { + WasmCompilerConfig() => Dart2WasmTarget(config), + JsCompilerConfig() => Dart2JSTarget(config), + } + ).toList() + ); - final WebRendererMode webRenderer; - final bool isWasm; + const WebReleaseBundle._withTargets(this.compileTargets); - String get outputFileNameNoSuffix => 'main.dart'; - String get outputFileName => '$outputFileNameNoSuffix${isWasm ? '.wasm' : '.js'}'; - String get wasmJSRuntimeFileName => '$outputFileNameNoSuffix.mjs'; + final List compileTargets; + + List get buildFiles => compileTargets.fold( + const Iterable.empty(), + (Iterable current, Dart2WebTarget target) => current.followedBy(target.buildFiles) + ).toList(); @override String get name => 'web_release_bundle'; @override - List get dependencies => [ - if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer), - ]; + List get dependencies => compileTargets; @override List get inputs => [ - Source.pattern('{BUILD_DIR}/$outputFileName'), const Source.pattern('{PROJECT_DIR}/pubspec.yaml'), - if (isWasm) Source.pattern('{BUILD_DIR}/$wasmJSRuntimeFileName'), ]; @override - List get outputs => [ - Source.pattern('{OUTPUT_DIR}/$outputFileName'), - if (isWasm) Source.pattern('{OUTPUT_DIR}/$wasmJSRuntimeFileName'), - ]; + List get outputs => []; @override List get depfiles => const [ - 'dart2js.d', 'flutter_assets.d', 'web_resources.d', ]; - bool shouldCopy(String name) => - // Do not copy the deps file. - (name.contains(outputFileName) && !name.endsWith('.deps')) || - (isWasm && name == wasmJSRuntimeFileName); - @override Future build(Environment environment) async { for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType()) { final String basename = environment.fileSystem.path.basename(outputFile.path); - if (shouldCopy(basename)) { + if (buildFiles.contains(basename)) { outputFile.copySync( environment.outputDir.childFile(environment.fileSystem.path.basename(outputFile.path)).path ); } } - if (isWasm) { - // TODO(jacksongardner): Enable icon tree shaking once dart2wasm can do a two-phase compile. - // https://github.com/flutter/flutter/issues/117248 - environment.defines[kIconTreeShakerFlag] = 'false'; - } - createVersionFile(environment, environment.defines); final Directory outputDirectory = environment.outputDir.childDirectory('assets'); outputDirectory.createSync(recursive: true); @@ -422,10 +467,19 @@ class WebReleaseBundle extends Target { // because it would need to be the hash for the entire bundle and not just the resource // in question. if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') { + final List> buildDescriptions = compileTargets.map( + (Dart2WebTarget target) => target.buildConfig + ).toList(); + final Map buildConfig = { + 'engineRevision': globals.flutterVersion.engineRevision, + 'builds': buildDescriptions, + }; + final String buildConfigString = '_flutter.buildConfig = ${jsonEncode(buildConfig)};'; final IndexHtml indexHtml = IndexHtml(inputFile.readAsStringSync()); indexHtml.applySubstitutions( baseHref: environment.defines[kBaseHref] ?? '/', serviceWorkerVersion: Random().nextInt(4294967296).toString(), + buildConfig: buildConfigString, ); outputFile.writeAsStringSync(indexHtml.content); continue; @@ -465,11 +519,9 @@ class WebReleaseBundle extends Target { /// These assets can be cached until a new version of the flutter web sdk is /// downloaded. class WebBuiltInAssets extends Target { - const WebBuiltInAssets(this.fileSystem, this.webRenderer, {required this.isWasm}); + const WebBuiltInAssets(this.fileSystem); final FileSystem fileSystem; - final WebRendererMode webRenderer; - final bool isWasm; @override String get name => 'web_static_assets'; @@ -500,7 +552,6 @@ class WebBuiltInAssets extends Target { @override List get outputs => [ - if (isWasm) const Source.pattern('{BUILD_DIR}/main.dart.js'), const Source.pattern('{BUILD_DIR}/flutter.js'), for (final File file in _canvasKitFiles) Source.pattern('{BUILD_DIR}/canvaskit/${_filePathRelativeToCanvasKitDirectory(file)}'), @@ -514,13 +565,6 @@ class WebBuiltInAssets extends Target { file.copySync(targetPath); } - if (isWasm) { - final File bootstrapFile = environment.outputDir.childFile('main.dart.js'); - bootstrapFile.writeAsStringSync( - wasm_bootstrap.generateWasmBootstrapFile(webRenderer == WebRendererMode.skwasm) - ); - } - // Write the flutter.js file final String flutterJsOut = fileSystem.path.join(environment.outputDir.path, 'flutter.js'); final File flutterJsFile = fileSystem.file(fileSystem.path.join( @@ -533,20 +577,18 @@ class WebBuiltInAssets extends Target { /// Generate a service worker for a web target. class WebServiceWorker extends Target { - const WebServiceWorker(this.fileSystem, this.webRenderer, {required this.isWasm}); + const WebServiceWorker(this.fileSystem, this.compileConfigs); final FileSystem fileSystem; - final WebRendererMode webRenderer; - final bool isWasm; + final List compileConfigs; @override String get name => 'web_service_worker'; @override List get dependencies => [ - if (isWasm) Dart2WasmTarget(webRenderer) else Dart2JSTarget(webRenderer), - WebReleaseBundle(webRenderer, isWasm: isWasm), - WebBuiltInAssets(fileSystem, webRenderer, isWasm: isWasm), + WebReleaseBundle(compileConfigs), + WebBuiltInAssets(fileSystem), ]; @override @@ -601,6 +643,10 @@ class WebServiceWorker extends Target { urlToHash, [ 'main.dart.js', + if (compileConfigs.any((WebCompilerConfig config) => config is WasmCompilerConfig)) ...[ + 'main.dart.wasm', + 'main.dart.mjs', + ], 'index.html', if (urlToHash.containsKey('assets/AssetManifest.bin.json')) 'assets/AssetManifest.bin.json', diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart index 52e2410ffa..7be2209167 100644 --- a/packages/flutter_tools/lib/src/commands/build_web.dart +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -138,24 +138,50 @@ class BuildWebCommand extends BuildSubCommand { throwToolExit('"build web" is not currently supported. To enable, run "flutter config --enable-web".'); } - final WebCompilerConfig compilerConfig; + final List compilerConfigs; if (boolArg('wasm')) { if (!featureFlags.isFlutterWebWasmEnabled) { throwToolExit('Compiling to WebAssembly (wasm) is only available on the master channel.'); } - compilerConfig = WasmCompilerConfig( - omitTypeChecks: boolArg('omit-type-checks'), - wasmOpt: WasmOptLevel.values.byName(stringArg('wasm-opt')!), + if (stringArg(FlutterOptions.kWebRendererFlag) != argParser.defaultFor(FlutterOptions.kWebRendererFlag)) { + throwToolExit('"--${FlutterOptions.kWebRendererFlag}" cannot be combined with "--${FlutterOptions.kWebWasmFlag}"'); + } + globals.logger.printBox( + title: 'Experimental feature', + ''' + WebAssembly compilation is experimental. + $kWasmMoreInfo''', ); + + compilerConfigs = [ + WasmCompilerConfig( + omitTypeChecks: boolArg('omit-type-checks'), + wasmOpt: WasmOptLevel.values.byName(stringArg('wasm-opt')!), + renderer: WebRendererMode.skwasm, + ), + JsCompilerConfig( + csp: boolArg('csp'), + optimizationLevel: stringArg('dart2js-optimization') ?? JsCompilerConfig.kDart2jsDefaultOptimizationLevel, + dumpInfo: boolArg('dump-info'), + nativeNullAssertions: boolArg('native-null-assertions'), + noFrequencyBasedMinification: boolArg('no-frequency-based-minification'), + sourceMaps: boolArg('source-maps'), + renderer: WebRendererMode.canvaskit, + )]; } else { - compilerConfig = JsCompilerConfig( + WebRendererMode webRenderer = WebRendererMode.auto; + if (argParser.options.containsKey(FlutterOptions.kWebRendererFlag)) { + webRenderer = WebRendererMode.values.byName(stringArg(FlutterOptions.kWebRendererFlag)!); + } + compilerConfigs = [JsCompilerConfig( csp: boolArg('csp'), optimizationLevel: stringArg('dart2js-optimization') ?? JsCompilerConfig.kDart2jsDefaultOptimizationLevel, dumpInfo: boolArg('dump-info'), nativeNullAssertions: boolArg('native-null-assertions'), noFrequencyBasedMinification: boolArg('no-frequency-based-minification'), sourceMaps: boolArg('source-maps'), - ); + renderer: webRenderer, + )]; } final FlutterProject flutterProject = FlutterProject.current(); @@ -205,7 +231,7 @@ class BuildWebCommand extends BuildSubCommand { target, buildInfo, ServiceWorkerStrategy.fromCliName(stringArg('pwa-strategy')), - compilerConfig: compilerConfig, + compilerConfigs: compilerConfigs, baseHref: baseHref, outputDirectoryPath: outputDirectoryPath, ); diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 03284538d3..16fbf80606 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -29,6 +29,7 @@ import '../runner/flutter_command.dart'; import '../runner/flutter_command_runner.dart'; import '../tracing.dart'; import '../vmservice.dart'; +import '../web/compile.dart'; import '../web/web_runner.dart'; import 'daemon.dart'; @@ -241,6 +242,10 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment final Map webHeaders = featureFlags.isWebEnabled ? extractWebHeaders() : const {}; + final String? webRendererString = stringArg('web-renderer'); + final WebRendererMode webRenderer = (webRendererString != null) + ? WebRendererMode.values.byName(webRendererString) + : WebRendererMode.auto; if (buildInfo.mode.isRelease) { return DebuggingOptions.disabled( @@ -258,6 +263,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, webHeaders: webHeaders, + webRenderer: webRenderer, enableImpeller: enableImpeller, enableVulkanValidation: enableVulkanValidation, uninstallFirst: uninstallFirst, @@ -307,6 +313,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, webHeaders: webHeaders, + webRenderer: webRenderer, vmserviceOutFile: stringArg('vmservice-out-file'), fastStart: argParser.options.containsKey('fast-start') && boolArg('fast-start') diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 69938a9b78..1f0cc0b241 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -18,6 +18,7 @@ import 'devfs.dart'; import 'device_port_forwarder.dart'; import 'project.dart'; import 'vmservice.dart'; +import 'web/compile.dart'; DeviceManager? get deviceManager => context.get(); @@ -952,6 +953,7 @@ class DebuggingOptions { this.webEnableExpressionEvaluation = false, this.webHeaders = const {}, this.webLaunchUrl, + this.webRenderer = WebRendererMode.auto, this.vmserviceOutFile, this.fastStart = false, this.nullAssertions = false, @@ -981,6 +983,7 @@ class DebuggingOptions { this.webBrowserFlags = const [], this.webLaunchUrl, this.webHeaders = const {}, + this.webRenderer = WebRendererMode.auto, this.cacheSkSL = false, this.traceAllowlist, this.enableImpeller = ImpellerStatus.platformDefault, @@ -1060,6 +1063,7 @@ class DebuggingOptions { required this.webEnableExpressionEvaluation, required this.webHeaders, required this.webLaunchUrl, + required this.webRenderer, required this.vmserviceOutFile, required this.fastStart, required this.nullAssertions, @@ -1144,6 +1148,9 @@ class DebuggingOptions { /// Allow developers to add custom headers to web server final Map webHeaders; + /// Which web renderer to use for the debugging session + final WebRendererMode webRenderer; + /// A file where the VM Service URL should be written after the application is started. final String? vmserviceOutFile; final bool fastStart; @@ -1252,6 +1259,7 @@ class DebuggingOptions { 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'webHeaders': webHeaders, + 'webRenderer': webRenderer.name, 'vmserviceOutFile': vmserviceOutFile, 'fastStart': fastStart, 'nullAssertions': nullAssertions, @@ -1307,6 +1315,7 @@ class DebuggingOptions { webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, webHeaders: (json['webHeaders']! as Map).cast(), webLaunchUrl: json['webLaunchUrl'] as String?, + webRenderer: WebRendererMode.values.byName(json['webRenderer']! as String), vmserviceOutFile: json['vmserviceOutFile'] as String?, fastStart: json['fastStart']! as bool, nullAssertions: json['nullAssertions']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 8f6241a5de..fa10cfb1cf 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -78,12 +78,14 @@ class WebDriverService extends DriverService { buildInfo, port: debuggingOptions.port, hostname: debuggingOptions.hostname, + webRenderer: debuggingOptions.webRenderer, ) : DebuggingOptions.enabled( buildInfo, port: debuggingOptions.port, hostname: debuggingOptions.hostname, disablePortPublication: debuggingOptions.disablePortPublication, + webRenderer: debuggingOptions.webRenderer, ), stayResident: true, flutterProject: FlutterProject.current(), diff --git a/packages/flutter_tools/lib/src/html_utils.dart b/packages/flutter_tools/lib/src/html_utils.dart index ad394aaa90..2477a90b2d 100644 --- a/packages/flutter_tools/lib/src/html_utils.dart +++ b/packages/flutter_tools/lib/src/html_utils.dart @@ -62,6 +62,7 @@ class IndexHtml { void applySubstitutions({ required String baseHref, required String? serviceWorkerVersion, + String? buildConfig, }) { if (_content.contains(kBaseHrefPlaceholder)) { _content = _content.replaceAll(kBaseHrefPlaceholder, baseHref); @@ -81,6 +82,12 @@ class IndexHtml { "navigator.serviceWorker.register('flutter_service_worker.js?v=$serviceWorkerVersion')", ); } + if (buildConfig != null) { + _content = _content.replaceFirst( + '{{flutter_build_config}}', + buildConfig, + ); + } } } diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index fcc1de0632..ac056760da 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -117,8 +117,9 @@ class WebAssetServer implements AssetReader { this.internetAddress, this._modules, this._digests, - this._nullSafetyMode, - ) : basePath = _getIndexHtml().getBaseHref(); + this._nullSafetyMode, { + required this.webRenderer, + }) : basePath = _getIndexHtml().getBaseHref(); // Fallback to "application/octet-stream" on null which // makes no claims as to the structure of the data. @@ -177,6 +178,7 @@ class WebAssetServer implements AssetReader { ExpressionCompiler? expressionCompiler, Map extraHeaders, NullSafetyMode nullSafetyMode, { + required WebRendererMode webRenderer, bool testMode = false, DwdsLauncher dwdsLauncher = Dwds.start, }) async { @@ -225,6 +227,7 @@ class WebAssetServer implements AssetReader { modules, digests, nullSafetyMode, + webRenderer: webRenderer, ); if (testMode) { return server; @@ -504,16 +507,29 @@ class WebAssetServer implements AssetReader { } /// Determines what rendering backed to use. - WebRendererMode webRenderer = WebRendererMode.html; + final WebRendererMode webRenderer; shelf.Response _serveIndex() { final IndexHtml indexHtml = _getIndexHtml(); + final Map buildConfig = { + 'engineRevision': globals.flutterVersion.engineRevision, + 'builds': [ + { + 'compileTarget': 'dartdevc', + 'renderer': webRenderer.name, + 'mainJsPath': 'main.dart.js', + }, + ], + }; + final String buildConfigString = '_flutter.buildConfig = ${jsonEncode(buildConfig)};'; + indexHtml.applySubstitutions( // Currently, we don't support --base-href for the "run" command. baseHref: '/', serviceWorkerVersion: null, + buildConfig: buildConfigString, ); final Map headers = { @@ -663,6 +679,7 @@ class WebDevFS implements DevFS { required this.nullAssertions, required this.nativeNullAssertions, required this.nullSafetyMode, + required this.webRenderer, this.testMode = false, }) : _port = port; @@ -686,6 +703,7 @@ class WebDevFS implements DevFS { final NullSafetyMode nullSafetyMode; final String? tlsCertPath; final String? tlsCertKeyPath; + final WebRendererMode webRenderer; late WebAssetServer webAssetServer; @@ -785,15 +803,11 @@ class WebDevFS implements DevFS { expressionCompiler, extraHeaders, nullSafetyMode, + webRenderer: webRenderer, testMode: testMode, ); final int selectedPort = webAssetServer.selectedPort; - if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) { - webAssetServer.webRenderer = WebRendererMode.auto; - } else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) { - webAssetServer.webRenderer = WebRendererMode.canvaskit; - } String url = '$hostname:$selectedPort'; if (hostname == 'any') { url ='localhost:$selectedPort'; diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 545df737bd..94e1a88c46 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -309,6 +309,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). nullAssertions: debuggingOptions.nullAssertions, nullSafetyMode: debuggingOptions.buildInfo.nullSafetyMode, nativeNullAssertions: debuggingOptions.nativeNullAssertions, + webRenderer: debuggingOptions.webRenderer, ); Uri url = await device!.devFS!.create(); if (debuggingOptions.tlsCertKeyPath != null && debuggingOptions.tlsCertPath != null) { @@ -339,7 +340,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). target, debuggingOptions.buildInfo, ServiceWorkerStrategy.none, - compilerConfig: JsCompilerConfig.run(nativeNullAssertions: debuggingOptions.nativeNullAssertions) + compilerConfigs: [ + JsCompilerConfig.run( + nativeNullAssertions: debuggingOptions.nativeNullAssertions, + renderer: debuggingOptions.webRenderer, + ) + ] ); } await device!.device!.startApp( @@ -418,7 +424,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). target, debuggingOptions.buildInfo, ServiceWorkerStrategy.none, - compilerConfig: JsCompilerConfig.run(nativeNullAssertions: debuggingOptions.nativeNullAssertions), + compilerConfigs: [ + JsCompilerConfig.run( + nativeNullAssertions: debuggingOptions.nativeNullAssertions, + renderer: debuggingOptions.webRenderer, + ) + ], ); } on ToolExit { return OperationResult(1, 'Failed to recompile application.'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index c96948452a..0b4d405883 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -137,7 +137,7 @@ class FlutterDevice { } final String platformDillPath = globals.fs.path.join( - getWebPlatformBinariesDirectory(globals.artifacts!, buildInfo.webRenderer).path, + globals.artifacts!.getHostArtifact(HostArtifact.webPlatformKernelFolder).path, platformDillName, ); diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 905b0fa254..6859b741b9 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1263,13 +1263,7 @@ abstract class FlutterCommand extends Command { : null; final Map defineConfigJsonMap = extractDartDefineConfigJsonMap(); - List dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap); - - WebRendererMode webRenderer = WebRendererMode.auto; - if (argParser.options.containsKey(FlutterOptions.kWebRendererFlag)) { - webRenderer = WebRendererMode.values.byName(stringArg(FlutterOptions.kWebRendererFlag)!); - dartDefines = updateDartDefines(dartDefines, webRenderer); - } + final List dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap); if (argParser.options.containsKey(FlutterOptions.kWebResourcesCdnFlag)) { final bool hasLocalWebSdk = argParser.options.containsKey('local-web-sdk') && stringArg('local-web-sdk') != null; @@ -1317,7 +1311,6 @@ abstract class FlutterCommand extends Command { dartDefines: dartDefines, bundleSkSLPath: bundleSkSLPath, dartExperiments: experiments, - webRenderer: webRenderer, performanceMeasurementFile: performanceMeasurementFile, packagesPath: packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'), nullSafetyMode: nullSafetyMode, @@ -1555,19 +1548,6 @@ abstract class FlutterCommand extends Command { return jsonEncode(propertyMap); } - /// Updates dart-defines based on [webRenderer]. - @visibleForTesting - static List updateDartDefines(List dartDefines, WebRendererMode webRenderer) { - final Set dartDefinesSet = dartDefines.toSet(); - if (!dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_AUTO_DETECT=')) - && dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA='))) { - dartDefinesSet.removeWhere((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA=')); - } - dartDefinesSet.addAll(webRenderer.dartDefines); - return dartDefinesSet.toList(); - } - - Map extractWebHeaders() { final Map webHeaders = {}; diff --git a/packages/flutter_tools/lib/src/test/web_test_compiler.dart b/packages/flutter_tools/lib/src/test/web_test_compiler.dart index ecf28ae080..706759004a 100644 --- a/packages/flutter_tools/lib/src/test/web_test_compiler.dart +++ b/packages/flutter_tools/lib/src/test/web_test_compiler.dart @@ -69,7 +69,7 @@ class WebTestCompiler { } final String platformDillPath = _fileSystem.path.join( - getWebPlatformBinariesDirectory(_artifacts, buildInfo.webRenderer).path, + _artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path, platformDillName ); diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index 9fb7073ec0..c16b0419b5 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -25,7 +25,6 @@ import '../version.dart'; import 'compiler_config.dart'; import 'file_generators/flutter_service_worker_js.dart'; import 'migrations/scrub_generated_plugin_registrant.dart'; -import 'web_constants.dart'; export 'compiler_config.dart'; @@ -59,23 +58,14 @@ class WebBuilder { String target, BuildInfo buildInfo, ServiceWorkerStrategy serviceWorkerStrategy, { - required WebCompilerConfig compilerConfig, + required List compilerConfigs, String? baseHref, String? outputDirectoryPath, }) async { - if (compilerConfig.isWasm) { - globals.logger.printBox( - title: 'Experimental feature', - ''' - WebAssembly compilation is experimental. - $kWasmMoreInfo''', - ); - } - final bool hasWebPlugins = (await findPlugins(flutterProject)).any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey)); final Directory outputDirectory = outputDirectoryPath == null - ? _fileSystem.directory(getWebBuildDirectory(compilerConfig.isWasm)) + ? _fileSystem.directory(getWebBuildDirectory()) : _fileSystem.directory(outputDirectoryPath); outputDirectory.createSync(recursive: true); @@ -91,7 +81,7 @@ class WebBuilder { final Stopwatch sw = Stopwatch()..start(); try { final BuildResult result = await _buildSystem.build( - WebServiceWorker(_fileSystem, buildInfo.webRenderer, isWasm: compilerConfig.isWasm), + WebServiceWorker(_fileSystem, compilerConfigs), Environment( projectDir: _fileSystem.currentDirectory, outputDir: outputDirectory, @@ -101,7 +91,6 @@ class WebBuilder { kHasWebPlugins: hasWebPlugins.toString(), if (baseHref != null) kBaseHref: baseHref, kServiceWorkerStrategy: serviceWorkerStrategy.cliName, - ...compilerConfig.toBuildSystemEnvironment(), ...buildInfo.toBuildSystemEnvironment(), }, artifacts: globals.artifacts!, @@ -134,8 +123,7 @@ class WebBuilder { } final String buildSettingsString = _buildEventAnalyticsSettings( - config: compilerConfig, - buildInfo: buildInfo, + configs: compilerConfigs, ); BuildEvent( @@ -151,14 +139,15 @@ class WebBuilder { )); final Duration elapsedDuration = sw.elapsed; + final String variableName = compilerConfigs.length > 1 ? 'dual-compile' : 'dart2js'; _flutterUsage.sendTiming( 'build', - compilerConfig.isWasm ? 'dart2wasm' : 'dart2js', + variableName, elapsedDuration, ); _analytics.send(Event.timing( workflow: 'build', - variableName: compilerConfig.isWasm ? 'dart2wasm' : 'dart2js', + variableName: variableName, elapsedMilliseconds: elapsedDuration.inMilliseconds, )); } @@ -245,13 +234,18 @@ const Map> kDartSdkJsMapArtif }; String _buildEventAnalyticsSettings({ - required WebCompilerConfig config, - required BuildInfo buildInfo, + required List configs, }) { - final Map values = { - ...config.buildEventAnalyticsValues, - 'web-renderer': buildInfo.webRenderer.cliName, - }; + final Map values = {}; + final List renderers = []; + final List targets = []; + for (final WebCompilerConfig config in configs) { + values.addAll(config.buildEventAnalyticsValues); + renderers.add(config.renderer.name); + targets.add(config.compileTarget.name); + } + values['web-renderer'] = renderers.join(','); + values['web-target'] = targets.join(','); final List sortedList = values.entries .map((MapEntry e) => '${e.key}: ${e.value};') diff --git a/packages/flutter_tools/lib/src/web/compiler_config.dart b/packages/flutter_tools/lib/src/web/compiler_config.dart index 286725b604..676c8dcc39 100644 --- a/packages/flutter_tools/lib/src/web/compiler_config.dart +++ b/packages/flutter_tools/lib/src/web/compiler_config.dart @@ -3,58 +3,48 @@ // found in the LICENSE file. import '../base/utils.dart'; +import '../convert.dart'; +import 'compile.dart'; -abstract class WebCompilerConfig { - const WebCompilerConfig(); +enum CompileTarget { + js, + wasm, +} - /// Returns `true` if `this` represents configuration for the Wasm compiler. - /// - /// Otherwise, `false`–represents the JavaScript compiler. - bool get isWasm; +sealed class WebCompilerConfig { + const WebCompilerConfig({required this.renderer}); - Map toBuildSystemEnvironment(); + /// Returns which target this compiler outputs (js or wasm) + CompileTarget get compileTarget; + final WebRendererMode renderer; - Map get buildEventAnalyticsValues => { - 'wasm-compile': isWasm, - }; + String get buildKey; + + Map get buildEventAnalyticsValues => {}; } /// Configuration for the Dart-to-Javascript compiler (dart2js). class JsCompilerConfig extends WebCompilerConfig { const JsCompilerConfig({ - required this.csp, - required this.dumpInfo, - required this.nativeNullAssertions, - required this.optimizationLevel, - required this.noFrequencyBasedMinification, - required this.sourceMaps, + this.csp = false, + this.dumpInfo = false, + this.nativeNullAssertions = false, + this.optimizationLevel = kDart2jsDefaultOptimizationLevel, + this.noFrequencyBasedMinification = false, + this.sourceMaps = true, + super.renderer = WebRendererMode.auto, }); /// Instantiates [JsCompilerConfig] suitable for the `flutter run` command. - const JsCompilerConfig.run({required bool nativeNullAssertions}) - : this( - csp: false, - dumpInfo: false, + const JsCompilerConfig.run({ + required bool nativeNullAssertions, + required WebRendererMode renderer, + }) : this( nativeNullAssertions: nativeNullAssertions, - noFrequencyBasedMinification: false, optimizationLevel: kDart2jsDefaultOptimizationLevel, - sourceMaps: true, + renderer: renderer, ); - /// Creates a new [JsCompilerConfig] from build system environment values. - /// - /// Should correspond exactly with [toBuildSystemEnvironment]. - factory JsCompilerConfig.fromBuildSystemEnvironment( - Map defines) => - JsCompilerConfig( - csp: defines[kCspMode] == 'true', - dumpInfo: defines[kDart2jsDumpInfo] == 'true', - nativeNullAssertions: defines[kNativeNullAssertions] == 'true', - optimizationLevel: defines[kDart2jsOptimization] ?? kDart2jsDefaultOptimizationLevel, - noFrequencyBasedMinification: defines[kDart2jsNoFrequencyBasedMinification] == 'true', - sourceMaps: defines[kSourceMapsEnabled] == 'true', - ); - /// The default optimization level for dart2js. /// /// Maps to [kDart2jsOptimization]. @@ -102,17 +92,7 @@ class JsCompilerConfig extends WebCompilerConfig { final bool sourceMaps; @override - bool get isWasm => false; - - @override - Map toBuildSystemEnvironment() => { - kCspMode: csp.toString(), - kDart2jsDumpInfo: dumpInfo.toString(), - kNativeNullAssertions: nativeNullAssertions.toString(), - kDart2jsNoFrequencyBasedMinification: noFrequencyBasedMinification.toString(), - kDart2jsOptimization: optimizationLevel, - kSourceMapsEnabled: sourceMaps.toString(), - }; + CompileTarget get compileTarget => CompileTarget.js; /// Arguments to use in both phases: full JS compile and CFE-only. List toSharedCommandOptions() => [ @@ -130,25 +110,29 @@ class JsCompilerConfig extends WebCompilerConfig { if (noFrequencyBasedMinification) '--no-frequency-based-minification', if (csp) '--csp', ]; + + @override + String get buildKey { + final Map settings = { + 'csp': csp, + 'dumpInfo': dumpInfo, + 'nativeNullAssertions': nativeNullAssertions, + 'noFrequencyBasedMinification': noFrequencyBasedMinification, + 'optimizationLevel': optimizationLevel, + 'sourceMaps': sourceMaps, + }; + return jsonEncode(settings); + } } /// Configuration for the Wasm compiler. class WasmCompilerConfig extends WebCompilerConfig { const WasmCompilerConfig({ - required this.omitTypeChecks, - required this.wasmOpt, + this.omitTypeChecks = false, + this.wasmOpt = WasmOptLevel.defaultValue, + super.renderer = WebRendererMode.auto, }); - /// Creates a new [WasmCompilerConfig] from build system environment values. - /// - /// Should correspond exactly with [toBuildSystemEnvironment]. - factory WasmCompilerConfig.fromBuildSystemEnvironment( - Map defines) => - WasmCompilerConfig( - omitTypeChecks: defines[kOmitTypeChecks] == 'true', - wasmOpt: WasmOptLevel.values.byName(defines[kRunWasmOpt]!), - ); - /// Build environment for [omitTypeChecks]. static const String kOmitTypeChecks = 'WasmOmitTypeChecks'; @@ -162,25 +146,31 @@ class WasmCompilerConfig extends WebCompilerConfig { final WasmOptLevel wasmOpt; @override - bool get isWasm => true; + CompileTarget get compileTarget => CompileTarget.wasm; - bool get runWasmOpt => wasmOpt == WasmOptLevel.full || wasmOpt == WasmOptLevel.debug; - - @override - Map toBuildSystemEnvironment() => { - kOmitTypeChecks: omitTypeChecks.toString(), - kRunWasmOpt: wasmOpt.name, - }; + bool get runWasmOpt => + wasmOpt == WasmOptLevel.full || wasmOpt == WasmOptLevel.debug; List toCommandOptions() => [ - if (omitTypeChecks) '--omit-type-checks', - ]; + if (omitTypeChecks) '--omit-type-checks', + ]; @override Map get buildEventAnalyticsValues => { - ...super.buildEventAnalyticsValues, - ...toBuildSystemEnvironment(), - }; + ...super.buildEventAnalyticsValues, + kOmitTypeChecks: omitTypeChecks.toString(), + kRunWasmOpt: wasmOpt.name, + }; + + @override + String get buildKey { + final Map settings = { + 'omitTypeChecks': omitTypeChecks, + 'wasmOpt': wasmOpt.name, + }; + return jsonEncode(settings); + } + } enum WasmOptLevel implements CliEnum { @@ -195,8 +185,11 @@ enum WasmOptLevel implements CliEnum { @override String get helpText => switch (this) { - WasmOptLevel.none => 'wasm-opt is not run. Fastest build; bigger, slower output.', - WasmOptLevel.debug => 'Similar to `${WasmOptLevel.full.name}`, but member names are preserved. Debugging is easier, but size is a bit bigger.', - WasmOptLevel.full => 'wasm-opt is run. Build time is slower, but output is smaller and faster.', - }; + WasmOptLevel.none => + 'wasm-opt is not run. Fastest build; bigger, slower output.', + WasmOptLevel.debug => + 'Similar to `${WasmOptLevel.full.name}`, but member names are preserved. Debugging is easier, but size is a bit bigger.', + WasmOptLevel.full => + 'wasm-opt is run. Build time is slower, but output is smaller and faster.', + }; } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart index 073bc90dfb..15a8ed0e22 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart @@ -11,11 +11,13 @@ import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/web.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build_web.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/web/compile.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -147,15 +149,9 @@ void main() { expect(environment.defines, { 'TargetFile': 'lib/main.dart', 'HasWebPlugins': 'true', - 'cspMode': 'false', - 'SourceMaps': 'false', - 'NativeNullAssertions': 'true', 'ServiceWorkerStrategy': 'offline-first', - 'Dart2jsDumpInfo': 'false', - 'Dart2jsNoFrequencyBasedMinification': 'false', - 'Dart2jsOptimization': 'O3', 'BuildMode': 'release', - 'DartDefines': 'Zm9vPWE=,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==', + 'DartDefines': 'Zm9vPWE=', 'DartObfuscation': 'false', 'TrackWidgetCreation': 'false', 'TreeShakeIcons': 'true', @@ -249,15 +245,8 @@ void main() { expect(environment.defines, { 'TargetFile': 'lib/main.dart', 'HasWebPlugins': 'true', - 'cspMode': 'false', - 'SourceMaps': 'false', - 'NativeNullAssertions': 'true', 'ServiceWorkerStrategy': 'offline-first', - 'Dart2jsDumpInfo': 'false', - 'Dart2jsNoFrequencyBasedMinification': 'false', - 'Dart2jsOptimization': 'O4', 'BuildMode': 'release', - 'DartDefines': 'RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==', 'DartObfuscation': 'false', 'TrackWidgetCreation': 'false', 'TreeShakeIcons': 'true', @@ -288,15 +277,17 @@ void main() { final CommandRunner runner = createTestCommandRunner(buildCommand); setupFileSystemForEndToEndTest(fileSystem); await runner.run(['build', 'web', '--no-pub']); - final BuildInfo buildInfo = - await buildCommand.webCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); - expect(buildInfo.dartDefines, contains('FLUTTER_WEB_AUTO_DETECT=true')); }, overrides: { Platform: () => fakePlatform, FileSystem: () => fileSystem, FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), ProcessManager: () => processManager, - BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)), + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) { + expect(target, isA()); + final List configs = (target as WebServiceWorker).compileConfigs; + expect(configs.length, 1); + expect(configs.first.renderer, WebRendererMode.auto); + }), }); testUsingContext('Web build supports build-name and build-number', () async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 8c097e6c51..96d944944b 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -31,7 +31,6 @@ import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/vmservice.dart'; -import 'package:flutter_tools/src/web/compile.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart' as analytics; import 'package:vm_service/vm_service.dart'; @@ -1086,47 +1085,6 @@ void main() { }); }); - group('dart-defines and web-renderer options', () { - late List dartDefines; - - setUp(() { - dartDefines = []; - }); - - test('auto web-renderer with no dart-defines', () { - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.auto); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); - }); - - test('canvaskit web-renderer with no dart-defines', () { - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); - }); - - test('html web-renderer with no dart-defines', () { - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); - }); - - test('auto web-renderer with existing dart-defines', () { - dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.auto); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); - }); - - test('canvaskit web-renderer with no dart-defines', () { - dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); - }); - - test('html web-renderer with no dart-defines', () { - dartDefines = ['FLUTTER_WEB_USE_SKIA=true']; - dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html); - expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); - }); - }); - group('terminal', () { late FakeAnsiTerminal fakeTerminal; diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart index 22bfa2ae94..a5c67ab92f 100644 --- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart @@ -21,10 +21,10 @@ import '../../src/fake_process_manager.dart'; void main() { late FileSystem fileSystem; late Environment environment; - late Target fooTarget; - late Target barTarget; - late Target fizzTarget; - late Target sharedTarget; + late TestTarget fooTarget; + late TestTarget barTarget; + late TestTarget fizzTarget; + late TestTarget sharedTarget; late int fooInvocations; late int barInvocations; late int shared; @@ -138,6 +138,23 @@ void main() { json.decode(stampFile.readAsStringSync())); expect(stampContents, containsPair('inputs', ['/foo.dart'])); + expect(stampContents!.containsKey('buildKey'), false); + }); + + testWithoutContext('Saves a stamp file with inputs, outputs and build key', () async { + fooTarget.buildKey = 'fooBuildKey'; + final BuildSystem buildSystem = setUpBuildSystem(fileSystem); + await buildSystem.build(fooTarget, environment); + final File stampFile = fileSystem.file( + '${environment.buildDir.path}/foo.stamp'); + + expect(stampFile, exists); + + final Map? stampContents = castStringKeyedMap( + json.decode(stampFile.readAsStringSync())); + + expect(stampContents, containsPair('inputs', ['/foo.dart'])); + expect(stampContents, containsPair('buildKey', 'fooBuildKey')); }); testWithoutContext('Creates a BuildResult with inputs and outputs', () async { @@ -168,6 +185,19 @@ void main() { expect(fooInvocations, 2); }); + testWithoutContext('Re-invoke build if build key is modified', () async { + final BuildSystem buildSystem = setUpBuildSystem(fileSystem); + fooTarget.buildKey = 'old'; + + await buildSystem.build(fooTarget, environment); + + fooTarget.buildKey = 'new'; + + await buildSystem.build(fooTarget, environment); + + expect(fooInvocations, 2); + }); + testWithoutContext('does not re-invoke build if input timestamp changes', () async { final BuildSystem buildSystem = setUpBuildSystem(fileSystem); await buildSystem.build(fooTarget, environment); @@ -723,4 +753,7 @@ class TestTarget extends Target { @override List outputs = []; + + @override + String? buildKey; } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_defines_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_defines_test.dart new file mode 100644 index 0000000000..71e04230a1 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_defines_test.dart @@ -0,0 +1,51 @@ +// Copyright 2014 The Flutter 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/build_system/targets/web.dart'; +import 'package:flutter_tools/src/web/compile.dart'; +import 'package:test/test.dart'; + +void main() { + + group('dart-defines and web-renderer options', () { + late List dartDefines; + + setUp(() { + dartDefines = []; + }); + + test('auto web-renderer with no dart-defines', () { + dartDefines = updateDartDefines(dartDefines, WebRendererMode.auto); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); + }); + + test('canvaskit web-renderer with no dart-defines', () { + dartDefines = updateDartDefines(dartDefines, WebRendererMode.canvaskit); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); + }); + + test('html web-renderer with no dart-defines', () { + dartDefines = updateDartDefines(dartDefines, WebRendererMode.html); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); + }); + + test('auto web-renderer with existing dart-defines', () { + dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; + dartDefines = updateDartDefines(dartDefines, WebRendererMode.auto); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); + }); + + test('canvaskit web-renderer with no dart-defines', () { + dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; + dartDefines = updateDartDefines(dartDefines, WebRendererMode.canvaskit); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); + }); + + test('html web-renderer with no dart-defines', () { + dartDefines = ['FLUTTER_WEB_USE_SKIA=true']; + dartDefines = updateDartDefines(dartDefines, WebRendererMode.html); + expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); + }); + }); +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index b4c0cbd7e4..7a5dc7b9ed 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -120,7 +120,9 @@ void main() { webResources.childFile('index.html') .createSync(recursive: true); environment.buildDir.childFile('main.dart.js').createSync(); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('version.json'), exists); })); @@ -132,7 +134,9 @@ void main() { final Directory webResources = environment.projectDir.childDirectory('web'); webResources.childFile('index.html').createSync(recursive: true); environment.buildDir.childFile('main.dart.js').createSync(); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); final String versionFile = environment.outputDir .childFile('version.json') @@ -150,7 +154,9 @@ void main() { '''); environment.buildDir.childFile('main.dart.js').createSync(); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/')); })); @@ -163,7 +169,9 @@ void main() { '''); environment.buildDir.childFile('main.dart.js').createSync(); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/')); })); @@ -185,7 +193,9 @@ void main() { .writeAsStringSync('A'); environment.buildDir.childFile('main.dart.js').createSync(); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('foo.txt') .readAsStringSync(), 'A'); @@ -197,7 +207,9 @@ void main() { // Update to arbitrary resource file triggers rebuild. webResources.childFile('foo.txt').writeAsStringSync('B'); - await const WebReleaseBundle(WebRendererMode.auto, isWasm: false).build(environment); + await WebReleaseBundle([ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('foo.txt') .readAsStringSync(), 'B'); @@ -354,6 +366,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -366,6 +379,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -376,7 +390,12 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + csp: true, + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -388,6 +407,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -400,6 +420,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -409,7 +430,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -422,6 +447,7 @@ void main() { ..._kDart2jsLinuxArgs, '--enable-experiment=non-nullable', '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -435,6 +461,7 @@ void main() { ..._kDart2jsLinuxArgs, '--enable-experiment=non-nullable', '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -444,7 +471,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -455,6 +486,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -467,6 +499,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -476,7 +509,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -487,6 +524,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -499,6 +537,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-O4', '-o', @@ -507,7 +546,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -519,6 +562,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--native-null-assertions', '--no-source-maps', '-o', @@ -532,6 +576,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--native-null-assertions', '--no-source-maps', '-O4', @@ -541,7 +586,12 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + nativeNullAssertions: true, + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -553,6 +603,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -565,6 +616,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-O3', '-o', @@ -573,7 +625,12 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + optimizationLevel: 'O3', + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -584,6 +641,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -599,6 +657,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-O4', '-o', @@ -607,7 +666,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); expect(environment.buildDir.childFile('dart2js.d'), exists); final Depfile depfile = environment.depFileService.parse(environment.buildDir.childFile('dart2js.d')); @@ -628,6 +691,7 @@ void main() { '-Ddart.vm.product=true', '-DFOO=bar', '-DBAZ=qux', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -642,6 +706,7 @@ void main() { '-Ddart.vm.product=true', '-DFOO=bar', '-DBAZ=qux', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-O4', '-o', @@ -650,7 +715,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -662,6 +731,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '-o', environment.buildDir.childFile('app.dill').absolute.path, '--packages=.dart_tool/package_config.json', @@ -673,6 +743,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '-O4', '-o', environment.buildDir.childFile('main.dart.js').absolute.path, @@ -680,7 +751,9 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig() + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -695,6 +768,7 @@ void main() { '-Ddart.vm.profile=true', '-DFOO=bar', '-DBAZ=qux', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -709,6 +783,7 @@ void main() { '-Ddart.vm.profile=true', '-DFOO=bar', '-DBAZ=qux', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -718,7 +793,11 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.auto).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -730,6 +809,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -742,6 +822,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -752,7 +833,12 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.canvaskit).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + dumpInfo: true, + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -764,6 +850,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-source-maps', '-o', environment.buildDir.childFile('app.dill').absolute.path, @@ -776,6 +863,7 @@ void main() { command: [ ..._kDart2jsLinuxArgs, '-Ddart.vm.profile=true', + '-DFLUTTER_WEB_AUTO_DETECT=true', '--no-minify', '--no-source-maps', '-O4', @@ -786,7 +874,12 @@ void main() { ] )); - await Dart2JSTarget(WebRendererMode.canvaskit).build(environment); + await Dart2JSTarget( + const JsCompilerConfig( + noFrequencyBasedMinification: true, + sourceMaps: false, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -805,6 +898,8 @@ void main() { '-Ddart.vm.profile=true', '-DFOO=bar', '-DBAZ=qux', + '-DFLUTTER_WEB_AUTO_DETECT=false', + '-DFLUTTER_WEB_USE_SKIA=true', '--depfile=${depFile.absolute.path}', environment.buildDir.childFile('main.dart').absolute.path, environment.buildDir.childFile('main.dart.unopt.wasm').absolute.path, @@ -821,7 +916,11 @@ void main() { ]) ); - await Dart2WasmTarget(WebRendererMode.canvaskit).build(environment); + await Dart2WasmTarget( + const WasmCompilerConfig( + renderer: WebRendererMode.canvaskit + ) + ).build(environment); expect(outputJsFile.existsSync(), isFalse); final File movedJsFile = environment.buildDir.childFile('main.dart.mjs'); @@ -843,6 +942,8 @@ void main() { command: [ ..._kDart2WasmLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=false', + '-DFLUTTER_WEB_USE_SKIA=true', '--omit-type-checks', '--depfile=${depFile.absolute.path}', environment.buildDir.childFile('main.dart').absolute.path, @@ -860,7 +961,12 @@ void main() { ]) ); - await Dart2WasmTarget(WebRendererMode.canvaskit).build(environment); + await Dart2WasmTarget( + const WasmCompilerConfig( + omitTypeChecks: true, + renderer: WebRendererMode.canvaskit + ) + ).build(environment); expect(outputJsFile.existsSync(), isFalse); final File movedJsFile = environment.buildDir.childFile('main.dart.mjs'); @@ -881,6 +987,8 @@ void main() { command: [ ..._kDart2WasmLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=false', + '-DFLUTTER_WEB_USE_SKIA=true', '--depfile=${depFile.absolute.path}', environment.buildDir.childFile('main.dart').absolute.path, environment.buildDir.childFile('main.dart.unopt.wasm').absolute.path, @@ -895,7 +1003,12 @@ void main() { environment.buildDir.childFile('main.dart.wasm').absolute.path, ])); - await Dart2WasmTarget(WebRendererMode.canvaskit).build(environment); + await Dart2WasmTarget( + const WasmCompilerConfig( + wasmOpt: WasmOptLevel.debug, + renderer: WebRendererMode.canvaskit + ) + ).build(environment); expect(outputJsFile.existsSync(), isFalse); final File movedJsFile = environment.buildDir.childFile('main.dart.mjs'); @@ -916,12 +1029,19 @@ void main() { command: [ ..._kDart2WasmLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=false', + '-DFLUTTER_WEB_USE_SKIA=true', '--depfile=${depFile.absolute.path}', environment.buildDir.childFile('main.dart').absolute.path, environment.buildDir.childFile('main.dart.wasm').absolute.path, ], onRun: (_) => outputJsFile..createSync()..writeAsStringSync('foo'))); - await Dart2WasmTarget(WebRendererMode.canvaskit).build(environment); + await Dart2WasmTarget( + const WasmCompilerConfig( + wasmOpt: WasmOptLevel.none, + renderer: WebRendererMode.canvaskit + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -936,6 +1056,9 @@ void main() { command: [ ..._kDart2WasmLinuxArgs, '-Ddart.vm.product=true', + '-DFLUTTER_WEB_AUTO_DETECT=false', + '-DFLUTTER_WEB_USE_SKIA=false', + '-DFLUTTER_WEB_USE_SKWASM=true', '--import-shared-memory', '--shared-memory-max-pages=32768', '--depfile=${depFile.absolute.path}', @@ -954,7 +1077,11 @@ void main() { ]) ); - await Dart2WasmTarget(WebRendererMode.skwasm).build(environment); + await Dart2WasmTarget( + const WasmCompilerConfig( + renderer: WebRendererMode.skwasm, + ) + ).build(environment); }, overrides: { ProcessManager: () => processManager, })); @@ -1002,7 +1129,9 @@ void main() { environment.outputDir.childDirectory('a').childFile('a.txt') ..createSync(recursive: true) ..writeAsStringSync('A'); - await WebServiceWorker(globals.fs, WebRendererMode.auto, isWasm: false).build(environment); + await WebServiceWorker(globals.fs, [ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('flutter_service_worker.js'), exists); // Contains file hash. @@ -1025,7 +1154,9 @@ void main() { .childFile('assets/index.html') ..createSync(recursive: true) ..writeAsStringSync('A'); - await WebServiceWorker(globals.fs, WebRendererMode.auto, isWasm: false).build(environment); + await WebServiceWorker(globals.fs, [ + const JsCompilerConfig() + ]).build(environment); expect(environment.outputDir.childFile('flutter_service_worker.js'), exists); // Contains the same file hash for both `/` and the root index.html file. @@ -1047,7 +1178,9 @@ void main() { environment.outputDir .childFile('main.dart.js.map') .createSync(recursive: true); - await WebServiceWorker(globals.fs, WebRendererMode.auto, isWasm: false).build(environment); + await WebServiceWorker(globals.fs, [ + const JsCompilerConfig() + ]).build(environment); // No caching of source maps. expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(), @@ -1057,24 +1190,12 @@ void main() { contains('"main.dart.js"')); })); - test('wasm build copies and generates specific files', () => testbed.run(() async { - globals.fs.file('bin/cache/flutter_web_sdk/canvaskit/canvaskit.wasm') - .createSync(recursive: true); - - await WebBuiltInAssets(globals.fs, WebRendererMode.auto, isWasm: true).build(environment); - - expect(environment.outputDir.childFile('main.dart.js').existsSync(), true); - expect(environment.outputDir.childDirectory('canvaskit') - .childFile('canvaskit.wasm') - .existsSync(), true); - })); - - test('wasm copies over canvaskit again if the web sdk changes', () => testbed.run(() async { + test('WebBuiltInAssets copies over canvaskit again if the web sdk changes', () => testbed.run(() async { final File canvasKitInput = globals.fs.file('bin/cache/flutter_web_sdk/canvaskit/canvaskit.wasm') ..createSync(recursive: true); canvasKitInput.writeAsStringSync('foo', flush: true); - await WebBuiltInAssets(globals.fs, WebRendererMode.auto, isWasm: true).build(environment); + await WebBuiltInAssets(globals.fs).build(environment); final File canvasKitOutputBefore = environment.outputDir.childDirectory('canvaskit') .childFile('canvaskit.wasm'); @@ -1083,7 +1204,7 @@ void main() { canvasKitInput.writeAsStringSync('bar', flush: true); - await WebBuiltInAssets(globals.fs, WebRendererMode.auto, isWasm: true).build(environment); + await WebBuiltInAssets(globals.fs).build(environment); final File canvasKitOutputAfter = environment.outputDir.childDirectory('canvaskit') .childFile('canvaskit.wasm'); diff --git a/packages/flutter_tools/test/general.shard/web/compile_web_test.dart b/packages/flutter_tools/test/general.shard/web/compile_web_test.dart index b3557413f8..39a8b12e61 100644 --- a/packages/flutter_tools/test/general.shard/web/compile_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/compile_web_test.dart @@ -43,16 +43,11 @@ void main() { testUsingContext('WebBuilder sets environment on success', () async { final TestBuildSystem buildSystem = TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) { - final WebServiceWorker webServiceWorker = target as WebServiceWorker; - expect(webServiceWorker.isWasm, isTrue, reason: 'should be wasm'); - expect(webServiceWorker.webRenderer, WebRendererMode.auto); - + expect(target, isA()); expect(environment.defines, { 'TargetFile': 'target', 'HasWebPlugins': 'false', 'ServiceWorkerStrategy': ServiceWorkerStrategy.offlineFirst.cliName, - 'WasmOmitTypeChecks': 'false', - 'RunWasmOpt': 'none', 'BuildMode': 'debug', 'DartObfuscation': 'false', 'TrackWidgetCreation': 'true', @@ -77,10 +72,16 @@ void main() { 'target', BuildInfo.debug, ServiceWorkerStrategy.offlineFirst, - compilerConfig: const WasmCompilerConfig( - omitTypeChecks: false, - wasmOpt: WasmOptLevel.none, - ), + compilerConfigs: [ + const WasmCompilerConfig( + wasmOpt: WasmOptLevel.none, + renderer: WebRendererMode.skwasm, + ), + const JsCompilerConfig.run( + nativeNullAssertions: true, + renderer: WebRendererMode.canvaskit, + ), + ], ); expect(logger.statusText, contains('Compiling target for the Web...')); @@ -102,7 +103,7 @@ void main() { label: 'web-compile', parameters: CustomDimensions( buildEventSettings: - 'RunWasmOpt: none; WasmOmitTypeChecks: false; wasm-compile: true; web-renderer: auto;', + 'RunWasmOpt: none; WasmOmitTypeChecks: false; web-renderer: skwasm,canvaskit; web-target: wasm,js;', ), ), ], @@ -115,7 +116,7 @@ void main() { Event.flutterBuildInfo( label: 'web-compile', buildType: 'web', - settings: 'RunWasmOpt: none; WasmOmitTypeChecks: false; wasm-compile: true; web-renderer: auto;', + settings: 'RunWasmOpt: none; WasmOmitTypeChecks: false; web-renderer: skwasm,canvaskit; web-target: wasm,js;', ), ]), ); @@ -123,12 +124,12 @@ void main() { // Sends timing event. final TestTimingEvent timingEvent = testUsage.timings.single; expect(timingEvent.category, 'build'); - expect(timingEvent.variableName, 'dart2wasm'); + expect(timingEvent.variableName, 'dual-compile'); expect( analyticsTimingEventExists( sentEvents: fakeAnalytics.sentEvents, workflow: 'build', - variableName: 'dart2wasm', + variableName: 'dual-compile', ), true, ); @@ -161,7 +162,9 @@ void main() { 'target', BuildInfo.debug, ServiceWorkerStrategy.offlineFirst, - compilerConfig: const JsCompilerConfig.run(nativeNullAssertions: true), + compilerConfigs: [ + const JsCompilerConfig.run(nativeNullAssertions: true, renderer: WebRendererMode.auto), + ] ), throwsToolExit(message: 'Failed to compile application for the Web.')); diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index 5034959c2e..0a52ef00d0 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -66,6 +66,7 @@ void main() { {}, {}, NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ); releaseAssetServer = ReleaseAssetServer( globals.fs.file('main.dart').uri, @@ -291,6 +292,7 @@ void main() { {}, {}, NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ); expect(webAssetServer.basePath, 'foo/bar'); @@ -310,6 +312,7 @@ void main() { {}, {}, NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ); // Defaults to "/" when there's no base element. @@ -331,6 +334,7 @@ void main() { {}, {}, NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ), throwsToolExit(), ); @@ -351,6 +355,7 @@ void main() { {}, {}, NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ), throwsToolExit(), ); @@ -684,6 +689,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.unsound, + webRenderer: WebRendererMode.html, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -745,13 +751,6 @@ void main() { // New SDK should be visible.. expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW'); - // Toggle CanvasKit - expect(webDevFS.webAssetServer.webRenderer, WebRendererMode.html); - webDevFS.webAssetServer.webRenderer = WebRendererMode.canvaskit; - - expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'OL'); - expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'CHUM'); - // Generated entrypoint. expect(await webDevFS.webAssetServer.dartSourceContents('web_entrypoint.dart'), contains('GENERATED')); @@ -800,6 +799,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.sound, + webRenderer: WebRendererMode.html, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.flutterJs.createSync(recursive: true); @@ -861,11 +861,6 @@ void main() { // New SDK should be visible.. expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW'); - // Toggle CanvasKit - webDevFS.webAssetServer.webRenderer = WebRendererMode.canvaskit; - expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'OL'); - expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'CHUM'); - // Generated entrypoint. expect(await webDevFS.webAssetServer.dartSourceContents('web_entrypoint.dart'), contains('GENERATED')); @@ -913,6 +908,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.sound, + webRenderer: WebRendererMode.canvaskit, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -974,6 +970,7 @@ void main() { nullAssertions: true, nativeNullAssertions: true, nullSafetyMode: NullSafetyMode.sound, + webRenderer: WebRendererMode.canvaskit, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1019,6 +1016,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.sound, + webRenderer: WebRendererMode.canvaskit, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1065,6 +1063,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.sound, + webRenderer: WebRendererMode.auto, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1112,6 +1111,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); @@ -1148,7 +1148,9 @@ void main() { null, const {}, NullSafetyMode.unsound, - testMode: true); + webRenderer: WebRendererMode.canvaskit, + testMode: true + ); expect(webAssetServer.defaultResponseHeaders['x-frame-options'], null); await webAssetServer.dispose(); @@ -1180,7 +1182,9 @@ void main() { extraHeaderKey: extraHeaderValue, }, NullSafetyMode.unsound, - testMode: true); + webRenderer: WebRendererMode.canvaskit, + testMode: true + ); expect(webAssetServer.defaultResponseHeaders[extraHeaderKey], [extraHeaderValue]); @@ -1215,6 +1219,7 @@ void main() { {}, {}, NullSafetyMode.sound, + webRenderer: WebRendererMode.canvaskit, ); expect(await webAssetServer.metadataContents('foo/main_module.ddc_merged_metadata'), null); @@ -1257,6 +1262,7 @@ void main() { extraHeaders: const {}, chromiumLauncher: null, nullSafetyMode: NullSafetyMode.unsound, + webRenderer: WebRendererMode.canvaskit, ); webDevFS.requireJS.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true); diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_wasm_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_wasm_test.dart index 4290db3375..40716aed39 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_build_wasm_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_build_wasm_test.dart @@ -51,7 +51,7 @@ void main() { final Directory appBuildDir = fileSystem.directory(fileSystem.path.join( exampleAppDir.path, 'build', - 'web_wasm', + 'web', )); for (final String filename in const [ 'flutter.js', diff --git a/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart b/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart index e01e0b9e1e..6220347af5 100644 --- a/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart +++ b/packages/flutter_tools/test/web.shard/hot_reload_web_test.dart @@ -19,6 +19,7 @@ void main() async { await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsCallback), name: 'flutter.js (callback)'); await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsPromisesFull), name: 'flutter.js (promises)'); await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsPromisesShort), name: 'flutter.js (promises, short)'); + await _testProject(HotReloadProject(indexHtml: indexHtmlFlutterJsLoad), name: 'flutter.js (load)'); await _testProject(HotReloadProject(indexHtml: indexHtmlNoFlutterJs), name: 'No flutter.js'); } diff --git a/packages/flutter_tools/test/web.shard/test_data/hot_reload_index_html_samples.dart b/packages/flutter_tools/test/web.shard/test_data/hot_reload_index_html_samples.dart index b1d5d2a18d..873604bf97 100644 --- a/packages/flutter_tools/test/web.shard/test_data/hot_reload_index_html_samples.dart +++ b/packages/flutter_tools/test/web.shard/test_data/hot_reload_index_html_samples.dart @@ -56,6 +56,27 @@ String indexHtmlFlutterJsPromisesFull = _generateFlutterJsIndexHtml(''' }); '''); +/// index_with_flutterjs.html +String indexHtmlFlutterJsLoad = _generateFlutterJsIndexHtml(''' + window.addEventListener('load', function(ev) { + _flutter.buildConfig = { + builds: [ + { + "compileTarget": "dartdevc", + "renderer": "html", + "mainJsPath": "main.dart.js", + } + ] + }; + // Download main.dart.js + _flutter.loader.load({ + serviceWorkerSettings: { + serviceWorkerVersion: serviceWorkerVersion, + }, + }); + }); +'''); + /// index_without_flutterjs.html String indexHtmlNoFlutterJs = '''