[flutter_tools] Enable hot reload on the web (#169174)

[flutter_tools] Enable hot reload on the web

Update the defaults so hot reload is enabled on web development builds
by default.
This enables the use of a new module representation in the compiled
JavaScript.
Passing `--no-web-experimental-hot-reload` will disable the ability to
hot reload
and return to the AMD JavaScript module representation.

This change avoids enabling hot reload in the flutter drive tests since
they rely on
`-d web-server` which has known startup issues. When
https://github.com/dart-lang/sdk/issues/60289 is
resolved it should be safe to enable hot reload by default for the
`flutter drive`
tests.

Fixes: https://github.com/flutter/flutter/issues/167510
This commit is contained in:
Nicholas Shahan 2025-06-02 16:13:50 -07:00 committed by GitHub
parent d4f60bddd2
commit 7e32a77210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 164 additions and 27 deletions

View File

@ -360,6 +360,8 @@ class WebTestsSuite {
'--browser-name=chrome',
'-d',
'web-server',
// TODO(nshahan): Remove when web-server can run with hot reload, https://github.com/dart-lang/sdk/issues/60289.
if (buildMode == 'debug') '--no-web-experimental-hot-reload',
'--$buildMode',
if (webRenderer == 'skwasm') ...<String>[
// See: WebRendererMode.dartDefines[skwasm]
@ -487,6 +489,7 @@ class WebTestsSuite {
'--browser-name=chrome',
'-d',
'web-server',
if (buildMode == 'debug') '--no-web-experimental-hot-reload',
'--$buildMode',
],
workingDirectory: testAppDirectory,

View File

@ -17,6 +17,7 @@ import 'base/os.dart';
import 'base/utils.dart';
import 'convert.dart';
import 'globals.dart' as globals;
import 'runner/flutter_command.dart' show FlutterOptions;
/// Whether icon font subsetting is enabled by default.
const bool kIconTreeShakerEnabledDefault = true;
@ -50,6 +51,7 @@ class BuildInfo {
this.assumeInitializeFromDillUpToDate = false,
this.buildNativeAssets = true,
this.useLocalCanvasKit = false,
this.webEnableHotReload = false,
}) : extraFrontEndOptions = extraFrontEndOptions ?? const <String>[],
extraGenSnapshotOptions = extraGenSnapshotOptions ?? const <String>[],
fileSystemRoots = fileSystemRoots ?? const <String>[],
@ -177,6 +179,9 @@ class BuildInfo {
/// If set, web builds will use the locally built CanvasKit instead of using the CDN
final bool useLocalCanvasKit;
/// If set, web builds with DDC will run with support for hot reload.
final bool webEnableHotReload;
/// Can be used when the actual information is not needed.
static const BuildInfo dummy = BuildInfo(
BuildMode.debug,
@ -259,13 +264,36 @@ class BuildInfo {
/// The module system DDC is targeting, or null if not using DDC or the
/// associated flag isn't present.
// TODO(markzipan): delete this when DDC's AMD module system is deprecated, https://github.com/flutter/flutter/issues/142060.
DdcModuleFormat? get ddcModuleFormat =>
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).ddcModuleFormat;
DdcModuleFormat get ddcModuleFormat {
final DdcModuleFormat moduleFormat =
webEnableHotReload ? DdcModuleFormat.ddc : DdcModuleFormat.amd;
final DdcModuleFormat? parsedFormat =
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).ddcModuleFormat;
if (parsedFormat != null && moduleFormat != parsedFormat) {
throw Exception(
'Unsupported option combination:\n'
'${FlutterOptions.kWebExperimentalHotReload}: $webEnableHotReload\n'
'${FlutterOptions.kExtraFrontEndOptions}: --dartdevc-module-format=${parsedFormat.name}',
);
}
return moduleFormat;
}
/// Whether to enable canary features when using DDC, or null if not using
/// DDC or the associated flag isn't present.
bool? get canaryFeatures =>
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).canaryFeatures;
bool get canaryFeatures {
final bool canaryEnabled = webEnableHotReload;
final bool? parsedCanary =
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).canaryFeatures;
if (parsedCanary != null && canaryEnabled != parsedCanary) {
throw Exception(
'Unsupported option combination:\n'
'${FlutterOptions.kWebExperimentalHotReload}: $webEnableHotReload\n'
'${FlutterOptions.kExtraFrontEndOptions}: --dartdevc-canary=$parsedCanary',
);
}
return canaryEnabled;
}
/// Convert to a structured string encoded structure appropriate for usage
/// in build system [Environment.defines].

View File

@ -375,7 +375,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
// connection.
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
extraFrontEndOptions: <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'],
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
packageConfig: PackageConfig.parseBytes(
widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),

View File

@ -167,12 +167,13 @@ class ResidentWebRunner extends ResidentRunner {
@override
bool get reloadIsRestart =>
debuggingOptions.webUseWasm ||
// Web behavior when not using the DDC library bundle format is to restart
// when a reload is issued. We can't use `canHotReload` to signal this
// since we still want a reload command to succeed, but to do a hot
// restart.
debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
debuggingOptions.buildInfo.canaryFeatures != true;
!debuggingOptions.buildInfo.canaryFeatures;
@override
bool get supportsDetach => stopAppDuringCleanup;
@ -321,7 +322,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
chromiumLauncher: _chromiumLauncher,
nativeNullAssertions: debuggingOptions.nativeNullAssertions,
ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc,
canaryFeatures: debuggingOptions.buildInfo.canaryFeatures ?? false,
canaryFeatures: debuggingOptions.buildInfo.canaryFeatures,
webRenderer: debuggingOptions.webRenderer,
isWasm: debuggingOptions.webUseWasm,
useLocalCanvasKit: debuggingOptions.buildInfo.useLocalCanvasKit,
@ -420,7 +421,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
final DateTime start = _systemClock.now();
final Status status;
if (debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
debuggingOptions.buildInfo.canaryFeatures == false) {
!debuggingOptions.buildInfo.canaryFeatures) {
// Triggering hot reload performed hot restart for the old module formats
// historically. Keep that behavior and only perform hot reload when the
// new module format is used.

View File

@ -114,6 +114,12 @@ class FlutterDevice {
globals.artifacts!.getHostArtifact(HostArtifact.webPlatformKernelFolder).path,
platformDillName,
);
final List<String> extraFrontEndOptions = <String>[
...buildInfo.extraFrontEndOptions,
if (buildInfo.webEnableHotReload)
// These flags are only valid to be passed when compiling with DDC.
...<String>['--dartdevc-canary', '--dartdevc-module-format=ddc'],
];
generator = ResidentCompiler(
globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path,
@ -133,7 +139,7 @@ class FlutterDevice {
assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate,
targetModel: TargetModel.dartdevc,
frontendServerStarterPath: buildInfo.frontendServerStarterPath,
extraFrontEndOptions: buildInfo.extraFrontEndOptions,
extraFrontEndOptions: extraFrontEndOptions,
platformDill: globals.fs.file(platformDillPath).absolute.uri.toString(),
dartDefines: buildInfo.dartDefines,
librariesSpec:

View File

@ -369,6 +369,7 @@ abstract class FlutterCommand extends Command<void> {
argParser.addFlag(
FlutterOptions.kWebExperimentalHotReload,
help: 'Enables new module format that supports hot reload.',
defaultsTo: true,
hide: !verboseHelp,
);
argParser.addOption(
@ -1325,10 +1326,9 @@ abstract class FlutterCommand extends Command<void> {
}
// TODO(natebiggs): Delete this when new DDC module system is the default.
if (argParser.options.containsKey(FlutterOptions.kWebExperimentalHotReload) &&
boolArg(FlutterOptions.kWebExperimentalHotReload)) {
extraFrontEndOptions.addAll(<String>['--dartdevc-canary', '--dartdevc-module-format=ddc']);
}
final bool webEnableHotReload =
argParser.options.containsKey(FlutterOptions.kWebExperimentalHotReload) &&
boolArg(FlutterOptions.kWebExperimentalHotReload);
String? codeSizeDirectory;
if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) &&
@ -1457,6 +1457,7 @@ abstract class FlutterCommand extends Command<void> {
argParser.options.containsKey(FlutterOptions.kAssumeInitializeFromDillUpToDate) &&
boolArg(FlutterOptions.kAssumeInitializeFromDillUpToDate),
useLocalCanvasKit: useLocalCanvasKit,
webEnableHotReload: webEnableHotReload,
);
}

View File

@ -283,7 +283,7 @@ class FlutterWebPlatform extends PlatformPlugin {
// TODO(srujzs): Remove this assertion when the library bundle format is
// supported without canary mode.
if (buildInfo.ddcModuleFormat == DdcModuleFormat.ddc) {
assert(buildInfo.canaryFeatures ?? true);
assert(buildInfo.canaryFeatures);
}
final Map<WebRendererMode, HostArtifact> dartSdkArtifactMap =
buildInfo.ddcModuleFormat == DdcModuleFormat.ddc
@ -296,7 +296,7 @@ class FlutterWebPlatform extends PlatformPlugin {
// TODO(srujzs): Remove this assertion when the library bundle format is
// supported without canary mode.
if (buildInfo.ddcModuleFormat == DdcModuleFormat.ddc) {
assert(buildInfo.canaryFeatures ?? true);
assert(buildInfo.canaryFeatures);
}
final Map<WebRendererMode, HostArtifact> dartSdkArtifactMap =
buildInfo.ddcModuleFormat == DdcModuleFormat.ddc

View File

@ -133,6 +133,7 @@ void main() {
packageConfigPath: '.dart_tool/package_config.json',
treeShakeIcons: false,
extraFrontEndOptions: <String>['--dartdevc-module-format=ddc', '--canary'],
webEnableHotReload: true,
),
webMemoryFS: WebMemoryFS(),
fileSystem: fileSystem,

View File

@ -735,7 +735,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
// TODO(nshahan): Remove when hot reload can no longer be disabled.
webEnableHotReload: true,
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),
@ -842,7 +843,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
// TODO(nshahan): Remove when hot reload can no longer be disabled.
webEnableHotReload: true,
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),
@ -927,7 +929,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
// TODO(nshahan): Remove when hot reload can no longer be disabled.
webEnableHotReload: true,
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
webUseWasm: true,
@ -996,13 +999,14 @@ name: my_app
// Test one extra config where `fullRestart` is false without the DDC library
// bundle format - we should do a hot restart in this case because hot reload
// is not available.
for (final (List<String> flags, bool fullRestart) in <(List<String>, bool)>[
(kDdcLibraryBundleFlags, true),
(<String>[], true),
(<String>[], false),
for (final (bool webEnableHotReload, bool fullRestart) in <(bool, bool)>[
(true, true),
(false, true),
(false, false),
]) {
testUsingContext(
'Can hot restart after attaching with flags: $flags fullRestart: $fullRestart',
'Can hot restart after attaching with '
'webEnableHotReload: $webEnableHotReload fullRestart: $fullRestart',
() async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
@ -1016,7 +1020,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
extraFrontEndOptions: flags,
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : const <String>[],
webEnableHotReload: webEnableHotReload,
),
),
);
@ -1242,8 +1247,9 @@ name: my_app
},
);
// TODO(nshahan): Delete this test case when hot reload can no longer be disabled.
testUsingContext(
'Fails non-fatally on vmservice response error for hot restart',
'Fails non-fatally on vmservice response error for hot restart (legacy default case)',
() async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(
@ -1261,6 +1267,8 @@ name: my_app
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
// Historically the .restart() would perform a hot restart even without
// passing fullRestart: true.
final OperationResult result = await residentWebRunner.restart();
expect(result.code, 0);
@ -1272,8 +1280,9 @@ name: my_app
},
);
// TODO(nshahan): Delete this test case when hot reload can no longer be disabled.
testUsingContext(
'Fails fatally on Vm Service error response',
'Fails fatally on Vm Service error response (legacy default case)',
() async {
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
fakeVmServiceHost = FakeVmServiceHost(
@ -1303,6 +1312,94 @@ name: my_app
},
);
for (final bool webEnableHotReload in <bool>[true, false]) {
testUsingContext(
'Fails non-fatally on vmservice response error for hot restart with webEnableHotReload: $webEnableHotReload',
() async {
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo(
BuildMode.debug,
null,
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
webEnableHotReload: webEnableHotReload,
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : <String>[],
),
),
);
fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: kHotRestartServiceName,
jsonResponse: <String, Object>{'type': 'Failed'},
),
],
);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(result.code, 0);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Pub: ThrowingPub.new,
},
);
testUsingContext(
'Fails fatally on Vm Service error response with webEnableHotReload: $webEnableHotReload',
() async {
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
debuggingOptions: DebuggingOptions.enabled(
BuildInfo(
BuildMode.debug,
null,
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
webEnableHotReload: webEnableHotReload,
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : <String>[],
),
),
);
fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
...kAttachExpectations,
FakeVmServiceRequest(
method: kHotRestartServiceName,
// Failed response,
error: FakeRPCError(code: vm_service.RPCErrorKind.kInternalError.code),
),
],
);
setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(result.code, 1);
expect(result.message, contains(vm_service.RPCErrorKind.kInternalError.code.toString()));
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Pub: ThrowingPub.new,
},
);
}
testUsingContext(
'printHelp without details shows only hot restart help message',
() async {
@ -1335,7 +1432,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
// TODO(nshahan): Remove when hot reload can no longer be disabled.
webEnableHotReload: true,
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),