// 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:process/process.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/project_migrator.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../build_system/build_system.dart'; import '../build_system/targets/web.dart'; import '../cache.dart'; import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../platform_plugins.dart'; import '../plugins.dart'; import '../project.dart'; import '../reporting/reporting.dart'; 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'; class WebBuilder { WebBuilder({ required Logger logger, required ProcessManager processManager, required BuildSystem buildSystem, required Usage usage, required FlutterVersion flutterVersion, required FileSystem fileSystem, }) : _logger = logger, _processManager = processManager, _buildSystem = buildSystem, _flutterUsage = usage, _flutterVersion = flutterVersion, _fileSystem = fileSystem; final Logger _logger; final ProcessManager _processManager; final BuildSystem _buildSystem; final Usage _flutterUsage; final FlutterVersion _flutterVersion; final FileSystem _fileSystem; Future buildWeb( FlutterProject flutterProject, String target, BuildInfo buildInfo, ServiceWorkerStrategy serviceWorkerStrategy, { required WebCompilerConfig compilerConfig, 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(outputDirectoryPath); outputDirectory.createSync(recursive: true); // The migrators to apply to a Web project. final List migrators = [ ScrubGeneratedPluginRegistrant(flutterProject.web, _logger), ]; final ProjectMigration migration = ProjectMigration(migrators); migration.run(); final Status status = _logger.startProgress('Compiling $target for the Web...'); final Stopwatch sw = Stopwatch()..start(); try { final BuildResult result = await _buildSystem.build( WebServiceWorker(_fileSystem, buildInfo.webRenderer, isWasm: compilerConfig.isWasm), Environment( projectDir: _fileSystem.currentDirectory, outputDir: outputDirectory, buildDir: flutterProject.directory.childDirectory('.dart_tool').childDirectory('flutter_build'), defines: { kTargetFile: target, kHasWebPlugins: hasWebPlugins.toString(), if (baseHref != null) kBaseHref: baseHref, kServiceWorkerStrategy: serviceWorkerStrategy.cliName, ...compilerConfig.toBuildSystemEnvironment(), ...buildInfo.toBuildSystemEnvironment(), }, artifacts: globals.artifacts!, fileSystem: _fileSystem, logger: _logger, processManager: _processManager, platform: globals.platform, usage: _flutterUsage, cacheDir: globals.cache.getRoot(), engineVersion: globals.artifacts!.isLocalEngine ? null : _flutterVersion.engineRevision, flutterRootDir: _fileSystem.directory(Cache.flutterRoot), // Web uses a different Dart plugin registry. // https://github.com/flutter/flutter/issues/80406 generateDartPluginRegistry: false, )); if (!result.success) { for (final ExceptionMeasurement measurement in result.exceptions.values) { _logger.printError( 'Target ${measurement.target} failed: ${measurement.exception}', stackTrace: measurement.fatal ? measurement.stackTrace : null, ); } throwToolExit('Failed to compile application for the Web.'); } } on Exception catch (err) { throwToolExit(err.toString()); } finally { status.stop(); } BuildEvent( 'web-compile', type: 'web', settings: _buildEventAnalyticsSettings( config: compilerConfig, buildInfo: buildInfo, ), flutterUsage: _flutterUsage, ).send(); _flutterUsage.sendTiming( 'build', compilerConfig.isWasm ? 'dart2wasm' : 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds), ); } } /// Web rendering backend mode. enum WebRendererMode implements CliEnum { /// Auto detects which rendering backend to use. auto, /// Always uses canvaskit. canvaskit, /// Always uses html. html, /// Always use skwasm. skwasm; @override String get cliName => snakeCase(name, '-'); @override String get helpText => switch (this) { auto => 'Use the HTML renderer on mobile devices, and CanvasKit on desktop devices.', canvaskit => 'Always use the CanvasKit renderer. This renderer uses WebGL and WebAssembly to render graphics.', html => 'Always use the HTML renderer. This renderer uses a combination of HTML, CSS, SVG, 2D Canvas, and WebGL.', skwasm => 'Always use the experimental skwasm renderer.' }; Iterable get dartDefines => switch (this) { WebRendererMode.auto => [ 'FLUTTER_WEB_AUTO_DETECT=true', ], WebRendererMode.canvaskit => [ 'FLUTTER_WEB_AUTO_DETECT=false', 'FLUTTER_WEB_USE_SKIA=true', ], WebRendererMode.html => [ 'FLUTTER_WEB_AUTO_DETECT=false', 'FLUTTER_WEB_USE_SKIA=false', ], WebRendererMode.skwasm => [ 'FLUTTER_WEB_AUTO_DETECT=false', 'FLUTTER_WEB_USE_SKIA=false', 'FLUTTER_WEB_USE_SKWASM=true', ] }; } /// The correct precompiled artifact to use for each build and render mode. const Map> kDartSdkJsArtifactMap = >{ WebRendererMode.auto: { NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitAndHtmlSoundSdk, NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitAndHtmlSdk, }, WebRendererMode.canvaskit: { NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitSoundSdk, NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitSdk, }, WebRendererMode.html: { NullSafetyMode.sound: HostArtifact.webPrecompiledSoundSdk, NullSafetyMode.unsound: HostArtifact.webPrecompiledSdk, }, }; /// The correct source map artifact to use for each build and render mode. const Map> kDartSdkJsMapArtifactMap = >{ WebRendererMode.auto: { NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitAndHtmlSoundSdkSourcemaps, NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitAndHtmlSdkSourcemaps, }, WebRendererMode.canvaskit: { NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitSoundSdkSourcemaps, NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitSdkSourcemaps, }, WebRendererMode.html: { NullSafetyMode.sound: HostArtifact.webPrecompiledSoundSdkSourcemaps, NullSafetyMode.unsound: HostArtifact.webPrecompiledSdkSourcemaps, }, }; String _buildEventAnalyticsSettings({ required WebCompilerConfig config, required BuildInfo buildInfo, }) { final Map values = { ...config.buildEventAnalyticsValues, 'web-renderer': buildInfo.webRenderer.cliName, }; final List sortedList = values.entries .map((MapEntry e) => '${e.key}: ${e.value};') .toList() ..sort(); return sortedList.join(' '); }