Skip to content

Commit 7e32a77

Browse files
authored
[flutter_tools] Enable hot reload on the web (flutter#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 dart-lang/sdk#60289 is resolved it should be safe to enable hot reload by default for the `flutter drive` tests. Fixes: flutter#167510
1 parent d4f60bd commit 7e32a77

File tree

9 files changed

+164
-27
lines changed

9 files changed

+164
-27
lines changed

dev/bots/suite_runners/run_web_tests.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ class WebTestsSuite {
360360
'--browser-name=chrome',
361361
'-d',
362362
'web-server',
363+
// TODO(nshahan): Remove when web-server can run with hot reload, https://github.com/dart-lang/sdk/issues/60289.
364+
if (buildMode == 'debug') '--no-web-experimental-hot-reload',
363365
'--$buildMode',
364366
if (webRenderer == 'skwasm') ...<String>[
365367
// See: WebRendererMode.dartDefines[skwasm]
@@ -487,6 +489,7 @@ class WebTestsSuite {
487489
'--browser-name=chrome',
488490
'-d',
489491
'web-server',
492+
if (buildMode == 'debug') '--no-web-experimental-hot-reload',
490493
'--$buildMode',
491494
],
492495
workingDirectory: testAppDirectory,

packages/flutter_tools/lib/src/build_info.dart

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'base/os.dart';
1717
import 'base/utils.dart';
1818
import 'convert.dart';
1919
import 'globals.dart' as globals;
20+
import 'runner/flutter_command.dart' show FlutterOptions;
2021

2122
/// Whether icon font subsetting is enabled by default.
2223
const bool kIconTreeShakerEnabledDefault = true;
@@ -50,6 +51,7 @@ class BuildInfo {
5051
this.assumeInitializeFromDillUpToDate = false,
5152
this.buildNativeAssets = true,
5253
this.useLocalCanvasKit = false,
54+
this.webEnableHotReload = false,
5355
}) : extraFrontEndOptions = extraFrontEndOptions ?? const <String>[],
5456
extraGenSnapshotOptions = extraGenSnapshotOptions ?? const <String>[],
5557
fileSystemRoots = fileSystemRoots ?? const <String>[],
@@ -177,6 +179,9 @@ class BuildInfo {
177179
/// If set, web builds will use the locally built CanvasKit instead of using the CDN
178180
final bool useLocalCanvasKit;
179181

182+
/// If set, web builds with DDC will run with support for hot reload.
183+
final bool webEnableHotReload;
184+
180185
/// Can be used when the actual information is not needed.
181186
static const BuildInfo dummy = BuildInfo(
182187
BuildMode.debug,
@@ -259,13 +264,36 @@ class BuildInfo {
259264
/// The module system DDC is targeting, or null if not using DDC or the
260265
/// associated flag isn't present.
261266
// TODO(markzipan): delete this when DDC's AMD module system is deprecated, https://github.com/flutter/flutter/issues/142060.
262-
DdcModuleFormat? get ddcModuleFormat =>
263-
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).ddcModuleFormat;
267+
DdcModuleFormat get ddcModuleFormat {
268+
final DdcModuleFormat moduleFormat =
269+
webEnableHotReload ? DdcModuleFormat.ddc : DdcModuleFormat.amd;
270+
final DdcModuleFormat? parsedFormat =
271+
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).ddcModuleFormat;
272+
if (parsedFormat != null && moduleFormat != parsedFormat) {
273+
throw Exception(
274+
'Unsupported option combination:\n'
275+
'${FlutterOptions.kWebExperimentalHotReload}: $webEnableHotReload\n'
276+
'${FlutterOptions.kExtraFrontEndOptions}: --dartdevc-module-format=${parsedFormat.name}',
277+
);
278+
}
279+
return moduleFormat;
280+
}
264281

265282
/// Whether to enable canary features when using DDC, or null if not using
266283
/// DDC or the associated flag isn't present.
267-
bool? get canaryFeatures =>
268-
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).canaryFeatures;
284+
bool get canaryFeatures {
285+
final bool canaryEnabled = webEnableHotReload;
286+
final bool? parsedCanary =
287+
_ddcModuleFormatAndCanaryFeaturesFromFrontEndArgs(extraFrontEndOptions).canaryFeatures;
288+
if (parsedCanary != null && canaryEnabled != parsedCanary) {
289+
throw Exception(
290+
'Unsupported option combination:\n'
291+
'${FlutterOptions.kWebExperimentalHotReload}: $webEnableHotReload\n'
292+
'${FlutterOptions.kExtraFrontEndOptions}: --dartdevc-canary=$parsedCanary',
293+
);
294+
}
295+
return canaryEnabled;
296+
}
269297

270298
/// Convert to a structured string encoded structure appropriate for usage
271299
/// in build system [Environment.defines].

packages/flutter_tools/lib/src/commands/widget_preview.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
375375
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
376376
// connection.
377377
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
378-
extraFrontEndOptions: <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'],
379378
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
380379
packageConfig: PackageConfig.parseBytes(
381380
widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),

packages/flutter_tools/lib/src/isolated/resident_web_runner.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,13 @@ class ResidentWebRunner extends ResidentRunner {
167167

168168
@override
169169
bool get reloadIsRestart =>
170+
debuggingOptions.webUseWasm ||
170171
// Web behavior when not using the DDC library bundle format is to restart
171172
// when a reload is issued. We can't use `canHotReload` to signal this
172173
// since we still want a reload command to succeed, but to do a hot
173174
// restart.
174175
debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
175-
debuggingOptions.buildInfo.canaryFeatures != true;
176+
!debuggingOptions.buildInfo.canaryFeatures;
176177

177178
@override
178179
bool get supportsDetach => stopAppDuringCleanup;
@@ -321,7 +322,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
321322
chromiumLauncher: _chromiumLauncher,
322323
nativeNullAssertions: debuggingOptions.nativeNullAssertions,
323324
ddcModuleSystem: debuggingOptions.buildInfo.ddcModuleFormat == DdcModuleFormat.ddc,
324-
canaryFeatures: debuggingOptions.buildInfo.canaryFeatures ?? false,
325+
canaryFeatures: debuggingOptions.buildInfo.canaryFeatures,
325326
webRenderer: debuggingOptions.webRenderer,
326327
isWasm: debuggingOptions.webUseWasm,
327328
useLocalCanvasKit: debuggingOptions.buildInfo.useLocalCanvasKit,
@@ -420,7 +421,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
420421
final DateTime start = _systemClock.now();
421422
final Status status;
422423
if (debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
423-
debuggingOptions.buildInfo.canaryFeatures == false) {
424+
!debuggingOptions.buildInfo.canaryFeatures) {
424425
// Triggering hot reload performed hot restart for the old module formats
425426
// historically. Keep that behavior and only perform hot reload when the
426427
// new module format is used.

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ class FlutterDevice {
114114
globals.artifacts!.getHostArtifact(HostArtifact.webPlatformKernelFolder).path,
115115
platformDillName,
116116
);
117+
final List<String> extraFrontEndOptions = <String>[
118+
...buildInfo.extraFrontEndOptions,
119+
if (buildInfo.webEnableHotReload)
120+
// These flags are only valid to be passed when compiling with DDC.
121+
...<String>['--dartdevc-canary', '--dartdevc-module-format=ddc'],
122+
];
117123

118124
generator = ResidentCompiler(
119125
globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path,
@@ -133,7 +139,7 @@ class FlutterDevice {
133139
assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate,
134140
targetModel: TargetModel.dartdevc,
135141
frontendServerStarterPath: buildInfo.frontendServerStarterPath,
136-
extraFrontEndOptions: buildInfo.extraFrontEndOptions,
142+
extraFrontEndOptions: extraFrontEndOptions,
137143
platformDill: globals.fs.file(platformDillPath).absolute.uri.toString(),
138144
dartDefines: buildInfo.dartDefines,
139145
librariesSpec:

packages/flutter_tools/lib/src/runner/flutter_command.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ abstract class FlutterCommand extends Command<void> {
369369
argParser.addFlag(
370370
FlutterOptions.kWebExperimentalHotReload,
371371
help: 'Enables new module format that supports hot reload.',
372+
defaultsTo: true,
372373
hide: !verboseHelp,
373374
);
374375
argParser.addOption(
@@ -1325,10 +1326,9 @@ abstract class FlutterCommand extends Command<void> {
13251326
}
13261327

13271328
// TODO(natebiggs): Delete this when new DDC module system is the default.
1328-
if (argParser.options.containsKey(FlutterOptions.kWebExperimentalHotReload) &&
1329-
boolArg(FlutterOptions.kWebExperimentalHotReload)) {
1330-
extraFrontEndOptions.addAll(<String>['--dartdevc-canary', '--dartdevc-module-format=ddc']);
1331-
}
1329+
final bool webEnableHotReload =
1330+
argParser.options.containsKey(FlutterOptions.kWebExperimentalHotReload) &&
1331+
boolArg(FlutterOptions.kWebExperimentalHotReload);
13321332

13331333
String? codeSizeDirectory;
13341334
if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) &&
@@ -1457,6 +1457,7 @@ abstract class FlutterCommand extends Command<void> {
14571457
argParser.options.containsKey(FlutterOptions.kAssumeInitializeFromDillUpToDate) &&
14581458
boolArg(FlutterOptions.kAssumeInitializeFromDillUpToDate),
14591459
useLocalCanvasKit: useLocalCanvasKit,
1460+
webEnableHotReload: webEnableHotReload,
14601461
);
14611462
}
14621463

packages/flutter_tools/lib/src/test/flutter_web_platform.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ class FlutterWebPlatform extends PlatformPlugin {
283283
// TODO(srujzs): Remove this assertion when the library bundle format is
284284
// supported without canary mode.
285285
if (buildInfo.ddcModuleFormat == DdcModuleFormat.ddc) {
286-
assert(buildInfo.canaryFeatures ?? true);
286+
assert(buildInfo.canaryFeatures);
287287
}
288288
final Map<WebRendererMode, HostArtifact> dartSdkArtifactMap =
289289
buildInfo.ddcModuleFormat == DdcModuleFormat.ddc
@@ -296,7 +296,7 @@ class FlutterWebPlatform extends PlatformPlugin {
296296
// TODO(srujzs): Remove this assertion when the library bundle format is
297297
// supported without canary mode.
298298
if (buildInfo.ddcModuleFormat == DdcModuleFormat.ddc) {
299-
assert(buildInfo.canaryFeatures ?? true);
299+
assert(buildInfo.canaryFeatures);
300300
}
301301
final Map<WebRendererMode, HostArtifact> dartSdkArtifactMap =
302302
buildInfo.ddcModuleFormat == DdcModuleFormat.ddc

packages/flutter_tools/test/commands.shard/hermetic/flutter_web_platform_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ void main() {
133133
packageConfigPath: '.dart_tool/package_config.json',
134134
treeShakeIcons: false,
135135
extraFrontEndOptions: <String>['--dartdevc-module-format=ddc', '--canary'],
136+
webEnableHotReload: true,
136137
),
137138
webMemoryFS: WebMemoryFS(),
138139
fileSystem: fileSystem,

packages/flutter_tools/test/general.shard/resident_web_runner_test.dart

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,8 @@ name: my_app
735735
trackWidgetCreation: true,
736736
treeShakeIcons: false,
737737
packageConfigPath: '.dart_tool/package_config.json',
738-
// Hot reload only supported with these flags for now.
738+
// TODO(nshahan): Remove when hot reload can no longer be disabled.
739+
webEnableHotReload: true,
739740
extraFrontEndOptions: kDdcLibraryBundleFlags,
740741
),
741742
),
@@ -842,7 +843,8 @@ name: my_app
842843
trackWidgetCreation: true,
843844
treeShakeIcons: false,
844845
packageConfigPath: '.dart_tool/package_config.json',
845-
// Hot reload only supported with these flags for now.
846+
// TODO(nshahan): Remove when hot reload can no longer be disabled.
847+
webEnableHotReload: true,
846848
extraFrontEndOptions: kDdcLibraryBundleFlags,
847849
),
848850
),
@@ -927,7 +929,8 @@ name: my_app
927929
trackWidgetCreation: true,
928930
treeShakeIcons: false,
929931
packageConfigPath: '.dart_tool/package_config.json',
930-
// Hot reload only supported with these flags for now.
932+
// TODO(nshahan): Remove when hot reload can no longer be disabled.
933+
webEnableHotReload: true,
931934
extraFrontEndOptions: kDdcLibraryBundleFlags,
932935
),
933936
webUseWasm: true,
@@ -996,13 +999,14 @@ name: my_app
996999
// Test one extra config where `fullRestart` is false without the DDC library
9971000
// bundle format - we should do a hot restart in this case because hot reload
9981001
// is not available.
999-
for (final (List<String> flags, bool fullRestart) in <(List<String>, bool)>[
1000-
(kDdcLibraryBundleFlags, true),
1001-
(<String>[], true),
1002-
(<String>[], false),
1002+
for (final (bool webEnableHotReload, bool fullRestart) in <(bool, bool)>[
1003+
(true, true),
1004+
(false, true),
1005+
(false, false),
10031006
]) {
10041007
testUsingContext(
1005-
'Can hot restart after attaching with flags: $flags fullRestart: $fullRestart',
1008+
'Can hot restart after attaching with '
1009+
'webEnableHotReload: $webEnableHotReload fullRestart: $fullRestart',
10061010
() async {
10071011
final BufferLogger logger = BufferLogger.test();
10081012
final ResidentRunner residentWebRunner = setUpResidentRunner(
@@ -1016,7 +1020,8 @@ name: my_app
10161020
trackWidgetCreation: true,
10171021
treeShakeIcons: false,
10181022
packageConfigPath: '.dart_tool/package_config.json',
1019-
extraFrontEndOptions: flags,
1023+
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : const <String>[],
1024+
webEnableHotReload: webEnableHotReload,
10201025
),
10211026
),
10221027
);
@@ -1242,8 +1247,9 @@ name: my_app
12421247
},
12431248
);
12441249

1250+
// TODO(nshahan): Delete this test case when hot reload can no longer be disabled.
12451251
testUsingContext(
1246-
'Fails non-fatally on vmservice response error for hot restart',
1252+
'Fails non-fatally on vmservice response error for hot restart (legacy default case)',
12471253
() async {
12481254
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
12491255
fakeVmServiceHost = FakeVmServiceHost(
@@ -1261,6 +1267,8 @@ name: my_app
12611267
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
12621268
await connectionInfoCompleter.future;
12631269

1270+
// Historically the .restart() would perform a hot restart even without
1271+
// passing fullRestart: true.
12641272
final OperationResult result = await residentWebRunner.restart();
12651273

12661274
expect(result.code, 0);
@@ -1272,8 +1280,9 @@ name: my_app
12721280
},
12731281
);
12741282

1283+
// TODO(nshahan): Delete this test case when hot reload can no longer be disabled.
12751284
testUsingContext(
1276-
'Fails fatally on Vm Service error response',
1285+
'Fails fatally on Vm Service error response (legacy default case)',
12771286
() async {
12781287
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
12791288
fakeVmServiceHost = FakeVmServiceHost(
@@ -1303,6 +1312,94 @@ name: my_app
13031312
},
13041313
);
13051314

1315+
for (final bool webEnableHotReload in <bool>[true, false]) {
1316+
testUsingContext(
1317+
'Fails non-fatally on vmservice response error for hot restart with webEnableHotReload: $webEnableHotReload',
1318+
() async {
1319+
final ResidentRunner residentWebRunner = setUpResidentRunner(
1320+
flutterDevice,
1321+
debuggingOptions: DebuggingOptions.enabled(
1322+
BuildInfo(
1323+
BuildMode.debug,
1324+
null,
1325+
trackWidgetCreation: true,
1326+
treeShakeIcons: false,
1327+
packageConfigPath: '.dart_tool/package_config.json',
1328+
webEnableHotReload: webEnableHotReload,
1329+
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : <String>[],
1330+
),
1331+
),
1332+
);
1333+
fakeVmServiceHost = FakeVmServiceHost(
1334+
requests: <VmServiceExpectation>[
1335+
...kAttachExpectations,
1336+
const FakeVmServiceRequest(
1337+
method: kHotRestartServiceName,
1338+
jsonResponse: <String, Object>{'type': 'Failed'},
1339+
),
1340+
],
1341+
);
1342+
setupMocks();
1343+
final Completer<DebugConnectionInfo> connectionInfoCompleter =
1344+
Completer<DebugConnectionInfo>();
1345+
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
1346+
await connectionInfoCompleter.future;
1347+
1348+
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
1349+
1350+
expect(result.code, 0);
1351+
},
1352+
overrides: <Type, Generator>{
1353+
FileSystem: () => fileSystem,
1354+
ProcessManager: () => processManager,
1355+
Pub: ThrowingPub.new,
1356+
},
1357+
);
1358+
1359+
testUsingContext(
1360+
'Fails fatally on Vm Service error response with webEnableHotReload: $webEnableHotReload',
1361+
() async {
1362+
final ResidentRunner residentWebRunner = setUpResidentRunner(
1363+
flutterDevice,
1364+
debuggingOptions: DebuggingOptions.enabled(
1365+
BuildInfo(
1366+
BuildMode.debug,
1367+
null,
1368+
trackWidgetCreation: true,
1369+
treeShakeIcons: false,
1370+
packageConfigPath: '.dart_tool/package_config.json',
1371+
webEnableHotReload: webEnableHotReload,
1372+
extraFrontEndOptions: webEnableHotReload ? kDdcLibraryBundleFlags : <String>[],
1373+
),
1374+
),
1375+
);
1376+
fakeVmServiceHost = FakeVmServiceHost(
1377+
requests: <VmServiceExpectation>[
1378+
...kAttachExpectations,
1379+
FakeVmServiceRequest(
1380+
method: kHotRestartServiceName,
1381+
// Failed response,
1382+
error: FakeRPCError(code: vm_service.RPCErrorKind.kInternalError.code),
1383+
),
1384+
],
1385+
);
1386+
setupMocks();
1387+
final Completer<DebugConnectionInfo> connectionInfoCompleter =
1388+
Completer<DebugConnectionInfo>();
1389+
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
1390+
await connectionInfoCompleter.future;
1391+
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
1392+
1393+
expect(result.code, 1);
1394+
expect(result.message, contains(vm_service.RPCErrorKind.kInternalError.code.toString()));
1395+
},
1396+
overrides: <Type, Generator>{
1397+
FileSystem: () => fileSystem,
1398+
ProcessManager: () => processManager,
1399+
Pub: ThrowingPub.new,
1400+
},
1401+
);
1402+
}
13061403
testUsingContext(
13071404
'printHelp without details shows only hot restart help message',
13081405
() async {
@@ -1335,7 +1432,8 @@ name: my_app
13351432
trackWidgetCreation: true,
13361433
treeShakeIcons: false,
13371434
packageConfigPath: '.dart_tool/package_config.json',
1338-
// Hot reload only supported with these flags for now.
1435+
// TODO(nshahan): Remove when hot reload can no longer be disabled.
1436+
webEnableHotReload: true,
13391437
extraFrontEndOptions: kDdcLibraryBundleFlags,
13401438
),
13411439
),

0 commit comments

Comments
 (0)