diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index fc8f3e7e6..ce0745325 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,3 +1,5 @@ +## 24.3.11-wip + ## 24.3.10 - Disabled breakpoints on changed files in a hot reload. They currently do not diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index 0380cfc8b..a107a046c 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '24.3.10'; +const packageVersion = '24.3.11-wip'; diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index e22f02ea0..18f4ee4c3 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -1,6 +1,6 @@ name: dwds # Every time this changes you need to run `dart run build_runner build`. -version: 24.3.10 +version: 24.3.11-wip description: >- A service that proxies between the Chrome debug protocol and the Dart VM service protocol. diff --git a/dwds/test/build_daemon_hot_restart_correctness_test.dart b/dwds/test/build_daemon_hot_restart_correctness_test.dart new file mode 100644 index 000000000..66042b6b6 --- /dev/null +++ b/dwds/test/build_daemon_hot_restart_correctness_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_correctness_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = false; + final moduleFormat = ModuleFormat.amd; + final compilationMode = CompilationMode.buildDaemon; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/build_daemon_hot_restart_test.dart b/dwds/test/build_daemon_hot_restart_test.dart new file mode 100644 index 000000000..12600703a --- /dev/null +++ b/dwds/test/build_daemon_hot_restart_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = false; + final moduleFormat = ModuleFormat.amd; + final compilationMode = CompilationMode.buildDaemon; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/common/hot_restart_common.dart b/dwds/test/common/hot_restart_common.dart new file mode 100644 index 000000000..deeb791b8 --- /dev/null +++ b/dwds/test/common/hot_restart_common.dart @@ -0,0 +1,647 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 5)) +library; + +import 'package:dwds/dwds.dart'; +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/logging.dart'; +import 'package:test_common/test_sdk_configuration.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../fixtures/context.dart'; +import '../fixtures/project.dart'; +import '../fixtures/utilities.dart'; + +const originalString = 'Hello World!'; +const newString = 'Bonjour le monde!'; + +void runTests({ + required TestSdkConfigurationProvider provider, + required ModuleFormat moduleFormat, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + final context = TestContext(TestProject.testAppendBody, provider); + + tearDownAll(provider.dispose); + + Future makeEditAndRecompile() async { + context.makeEditToDartEntryFile( + toReplace: originalString, + replaceWith: newString, + ); + if (compilationMode == CompilationMode.frontendServer) { + await context.recompile(fullRestart: true); + } else { + assert(compilationMode == CompilationMode.buildDaemon); + await context.waitForSuccessfulBuild(propagateToBrowser: true); + } + } + + void undoEdit() { + context.makeEditToDartEntryFile( + toReplace: newString, + replaceWith: originalString, + ); + } + + group( + 'Injected client with live reload', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + + group('and without debugging using WebSockets', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + useSse: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can live reload changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); + + group('Injected client', () { + late VmService fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + fakeClient = await context.connectFakeClient(); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('destroys and recreates the isolate during a hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + }); + + test('can execute simultaneous hot restarts', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + // Execute two hot restart calls in parallel. + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + final done = Future.wait([ + fakeClient.callServiceExtension(hotRestart!), + fakeClient.callServiceExtension(hotRestart), + ]); + expect(await done, [ + const TypeMatcher(), + const TypeMatcher(), + ]); + + // The debugger is still working. + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + + final result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + + await eventsDone; + }); + + test('destroys and recreates the isolate during a page refresh', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await context.webDriver.driver.refresh(); + + await eventsDone; + }); + + test('can hot restart via the service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Main is re-invoked which shouldn't clear the state. + expect(source, contains(originalString)); + expect(source, contains(newString)); + }); + + test('can send events before and after hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + // The event just before hot restart might never be received, + // but the injected client continues to work and send events + // after hot restart. + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + _hasKind( + EventKind.kServiceExtensionAdded, + ).having((e) => e.extensionRPC, 'service', 'ext.bar'), + ), + ); + + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + var isolate = await client.getIsolate(isolateId); + var library = isolate.rootLib!.uri!; + + final callback = '(_, __) async => ServiceExtensionResponse.result("")'; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.foo', $callback)", + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + isolate = await client.getIsolate(isolateId); + library = isolate.rootLib!.uri!; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.bar', $callback)", + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Main is re-invoked which shouldn't clear the state. + expect(source, contains('Hello World!')); + }); + + test('can refresh the page via the fullReload service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final fullReload = context.getRegisteredServiceExtension('fullReload'); + expect( + await fakeClient.callServiceExtension(fullReload!), + isA(), + ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Should see only the new text + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); + + test('can hot restart while paused', () async { + final client = context.debugConnection.vmService; + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + await client.streamListen('Debug'); + final stream = client.onEvent('Debug'); + final scriptList = await client.getScripts(isolateId); + final main = scriptList.scripts!.firstWhere( + (script) => script.uri!.contains('main.dart'), + ); + final bpLine = await context.findBreakpointLine( + 'printCount', + isolateId, + main, + ); + await client.addBreakpoint(isolateId, main.id!, bpLine); + await stream.firstWhere( + (event) => event.kind == EventKind.kPauseBreakpoint, + ); + + await makeEditAndRecompile(); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + + // Previous breakpoint should be cleared. + expect(isolate.breakpoints!.isEmpty, isTrue); + }); + + test('can evaluate expressions after hot restart', () async { + final client = context.debugConnection.vmService; + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + + // Expression evaluation while running should work. + final result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + }); + }, timeout: Timeout.factor(2)); + + group( + 'Injected client with hot restart', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); + + test( + 'fires isolate create/destroy events during hot restart', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await makeEditAndRecompile(); + + await eventsDone; + }, + ); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndRecompile(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); + + group( + 'when isolates_paused_on_start is true', + () { + late VmService client; + late VmService fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + client = context.debugConnection.vmService; + fakeClient = await context.connectFakeClient(); + await client.setFlag('pause_isolates_on_start', 'true'); + await client.streamListen('Isolate'); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test( + 'after hot-restart, does not run app until there is a resume event', + () async { + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension( + 'hotRestart', + ); + expect( + await fakeClient.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + final sourceBeforeResume = await context.webDriver.pageSource; + expect(sourceBeforeResume.contains(newString), isFalse); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + await client.resume(isolateId); + + final sourceAfterResume = await context.webDriver.pageSource; + expect(sourceAfterResume.contains(newString), isTrue); + }, + ); + + test( + 'after page refresh, does not run app until there is a resume event', + () async { + await makeEditAndRecompile(); + + await context.webDriver.driver.refresh(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await eventsDone; + + final sourceBeforeResume = await context.webDriver.pageSource; + expect(sourceBeforeResume.contains(newString), isFalse); + + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + await client.resume(isolateId); + + final sourceAfterResume = await context.webDriver.pageSource; + expect(sourceAfterResume.contains(newString), isTrue); + }, + ); + }, + // https://github.com/dart-lang/sdk/issues/60528 + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true, + ); +} + +TypeMatcher _hasKind(String kind) => + isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/common/hot_restart_correctness_common.dart b/dwds/test/common/hot_restart_correctness_common.dart new file mode 100644 index 000000000..5cdda1000 --- /dev/null +++ b/dwds/test/common/hot_restart_correctness_common.dart @@ -0,0 +1,225 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 5)) +library; + +import 'package:dwds/dwds.dart'; +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/logging.dart'; +import 'package:test_common/test_sdk_configuration.dart'; +import 'package:test_common/utilities.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../fixtures/context.dart'; +import '../fixtures/project.dart'; +import '../fixtures/utilities.dart'; + +const originalString = 'variableToModifyToForceRecompile = 23'; +const newString = 'variableToModifyToForceRecompile = 45'; + +const constantSuccessString = 'ConstantEqualitySuccess'; +const constantFailureString = 'ConstantEqualityFailure'; + +void runTests({ + required TestSdkConfigurationProvider provider, + required ModuleFormat moduleFormat, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + tearDownAll(provider.dispose); + + final testHotRestart2 = TestProject.testHotRestart2; + final context = TestContext(testHotRestart2, provider); + + Future makeEditAndRecompile() async { + context.makeEditToDartLibFile( + libFileName: 'library2.dart', + toReplace: originalString, + replaceWith: newString, + ); + if (compilationMode == CompilationMode.frontendServer) { + await context.recompile(fullRestart: true); + } else { + assert(compilationMode == CompilationMode.buildDaemon); + await context.waitForSuccessfulBuild(propagateToBrowser: true); + } + } + + void undoEdit() { + context.makeEditToDartLibFile( + libFileName: 'library2.dart', + toReplace: newString, + replaceWith: originalString, + ); + } + + group('Injected client', () { + VmService? fakeClient; + + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + + fakeClient = await context.connectFakeClient(); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test( + 'properly compares constants after hot restart via the service extension', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + var source = await context.webDriver.pageSource; + expect( + source, + contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), + ); + + await makeEditAndRecompile(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + expect( + await fakeClient!.callServiceExtension(hotRestart!), + const TypeMatcher(), + ); + + await eventsDone; + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }, + ); + }, timeout: Timeout.factor(2)); + + group( + 'Injected client with hot restart', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('properly compares constants after hot restart', () async { + var source = await context.webDriver.pageSource; + expect( + source, + contains( + 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', + ), + ); + + await makeEditAndRecompile(); + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('properly compares constants after hot restart', () async { + var source = await context.webDriver.pageSource; + expect( + source, + contains( + 'ConstObject(reloadVariable: 23, ConstantEqualitySuccess)', + ), + ); + + await makeEditAndRecompile(); + + source = await context.webDriver.pageSource; + if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { + expect( + source, + contains( + 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', + ), + ); + } + }); + }); + }, + // `BuildResult`s are only ever emitted when using the build daemon. + skip: compilationMode != CompilationMode.buildDaemon, + timeout: Timeout.factor(2), + ); +} + +TypeMatcher _hasKind(String kind) => + isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 220301050..1d9f497fb 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -115,6 +115,10 @@ class TestContext { final _serviceNameToMethod = {}; + late LocalFileSystem frontendServerFileSystem; + + late String _hostname; + /// Internal VM service. /// /// Prefer using [vmService] instead in tests when possible, to include testing @@ -305,9 +309,9 @@ class TestContext { final entry = p.toUri( p.join(project.webAssetsPath, project.dartEntryFileName), ); - final fileSystem = LocalFileSystem(); + frontendServerFileSystem = LocalFileSystem(); final packageUriMapper = await PackageUriMapper.create( - fileSystem, + frontendServerFileSystem, project.packageConfigFile, useDebuggerModuleNames: testSettings.useDebuggerModuleNames, ); @@ -333,20 +337,23 @@ class TestContext { ); final assetServerPort = await findUnusedPort(); + _hostname = appMetadata.hostname; await webRunner.run( - fileSystem, - appMetadata.hostname, + frontendServerFileSystem, + _hostname, assetServerPort, filePathToServe, + initialCompile: true, + fullRestart: false, ); if (testSettings.enableExpressionEvaluation) { expressionCompiler = webRunner.expressionCompiler; } - basePath = webRunner.devFS.assetServer.basePath; - assetReader = webRunner.devFS.assetServer; - _assetHandler = webRunner.devFS.assetServer.handleRequest; + basePath = webRunner.devFS!.assetServer.basePath; + assetReader = webRunner.devFS!.assetServer; + _assetHandler = webRunner.devFS!.assetServer.handleRequest; loadStrategy = switch (testSettings.moduleFormat) { ModuleFormat.amd => FrontendServerRequireStrategyProvider( @@ -572,6 +579,18 @@ class TestContext { file.writeAsStringSync(fileContents.replaceAll(toReplace, replaceWith)); } + Future recompile({required bool fullRestart}) async { + await webRunner.run( + frontendServerFileSystem, + _hostname, + await findUnusedPort(), + webCompatiblePath([project.directoryToServe, project.filePathToServe]), + initialCompile: false, + fullRestart: fullRestart, + ); + return; + } + Future waitForSuccessfulBuild({ Duration? timeout, bool propagateToBrowser = false, diff --git a/dwds/test/frontend_server_hot_restart_correctness_test.dart b/dwds/test/frontend_server_hot_restart_correctness_test.dart new file mode 100644 index 000000000..198e6546c --- /dev/null +++ b/dwds/test/frontend_server_hot_restart_correctness_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_correctness_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final moduleFormat = ModuleFormat.ddc; + final compilationMode = CompilationMode.frontendServer; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); +} diff --git a/dwds/test/frontend_server_hot_restart_test.dart b/dwds/test/frontend_server_hot_restart_test.dart new file mode 100644 index 000000000..95d98bd28 --- /dev/null +++ b/dwds/test/frontend_server_hot_restart_test.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'common/hot_restart_common.dart'; +import 'fixtures/context.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final moduleFormat = ModuleFormat.ddc; + final compilationMode = CompilationMode.frontendServer; + + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} diff --git a/dwds/test/reload_correctness_test.dart b/dwds/test/reload_correctness_test.dart deleted file mode 100644 index ffffc6a6f..000000000 --- a/dwds/test/reload_correctness_test.dart +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -@Tags(['daily']) -@TestOn('vm') -@Timeout(Duration(minutes: 5)) -library; - -import 'package:dwds/dwds.dart'; -import 'package:test/test.dart'; -import 'package:test_common/logging.dart'; -import 'package:test_common/test_sdk_configuration.dart'; -import 'package:test_common/utilities.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'fixtures/context.dart'; -import 'fixtures/project.dart'; -import 'fixtures/utilities.dart'; - -const originalString = 'variableToModifyToForceRecompile = 23'; -const newString = 'variableToModifyToForceRecompile = 45'; - -const constantSuccessString = 'ConstantEqualitySuccess'; -const constantFailureString = 'ConstantEqualityFailure'; - -void main() { - // set to true for debug logging. - final debug = false; - - final provider = TestSdkConfigurationProvider(verbose: debug); - tearDownAll(provider.dispose); - - final testHotRestart2 = TestProject.testHotRestart2; - final context = TestContext(testHotRestart2, provider); - - Future makeEditAndWaitForRebuild() async { - context.makeEditToDartLibFile( - libFileName: 'library2.dart', - toReplace: originalString, - replaceWith: newString, - ); - await context.waitForSuccessfulBuild(propagateToBrowser: true); - } - - void undoEdit() { - context.makeEditToDartLibFile( - libFileName: 'library2.dart', - toReplace: newString, - replaceWith: originalString, - ); - } - - group('Injected client', () { - VmService? fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - - fakeClient = await context.connectFakeClient(); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test( - 'properly compares constants after hot restart via the service extension', - () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient!.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }, - ); - }, timeout: Timeout.factor(2)); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('properly compares constants after hot restart', () async { - var source = await context.webDriver.pageSource; - expect( - source, - contains('ConstObject(reloadVariable: 23, ConstantEqualitySuccess)'), - ); - - await makeEditAndWaitForRebuild(); - - source = await context.webDriver.pageSource; - if (dartSdkIsAtLeast('3.4.0-61.0.dev')) { - expect( - source, - contains( - 'ConstObject(reloadVariable: 45, ConstantEqualitySuccess)', - ), - ); - } - }); - }); - }, timeout: Timeout.factor(2)); -} - -TypeMatcher _hasKind(String kind) => - isA().having((e) => e.kind, 'kind', kind); diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart deleted file mode 100644 index b0451104a..000000000 --- a/dwds/test/reload_test.dart +++ /dev/null @@ -1,594 +0,0 @@ -// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -@Tags(['daily']) -@TestOn('vm') -@Timeout(Duration(minutes: 5)) -library; - -import 'package:dwds/dwds.dart'; -import 'package:test/test.dart'; -import 'package:test_common/logging.dart'; -import 'package:test_common/test_sdk_configuration.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'fixtures/context.dart'; -import 'fixtures/project.dart'; -import 'fixtures/utilities.dart'; - -const originalString = 'Hello World!'; -const newString = 'Bonjour le monde!'; - -void main() { - // set to true for debug logging. - final debug = false; - - final provider = TestSdkConfigurationProvider(verbose: debug); - tearDownAll(provider.dispose); - - final context = TestContext(TestProject.testAppendBody, provider); - - Future makeEditAndWaitForRebuild() async { - context.makeEditToDartEntryFile( - toReplace: originalString, - replaceWith: newString, - ); - await context.waitForSuccessfulBuild(propagateToBrowser: true); - } - - void undoEdit() { - context.makeEditToDartEntryFile( - toReplace: newString, - replaceWith: originalString, - ); - } - - group('Injected client with live reload', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - ); - }); - - tearDown(() async { - undoEdit(); - await context.tearDown(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - undoEdit(); - await context.tearDown(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - - group('and without debugging using WebSockets', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - useSse: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - }, timeout: Timeout.factor(2)); - - group('Injected client', () { - late VmService fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - fakeClient = await context.connectFakeClient(); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('destroys and recreates the isolate during a hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - }); - - test('can execute simultaneous hot restarts', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - // Execute two hot restart calls in parallel. - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - final done = Future.wait([ - fakeClient.callServiceExtension(hotRestart!), - fakeClient.callServiceExtension(hotRestart), - ]); - expect(await done, [ - const TypeMatcher(), - const TypeMatcher(), - ]); - - // The debugger is still working. - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - - final result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - - await eventsDone; - }); - - test('destroys and recreates the isolate during a page refresh', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await context.webDriver.driver.refresh(); - - await eventsDone; - }); - - test('can hot restart via the service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains(originalString)); - expect(source, contains(newString)); - }); - - test('can send events before and after hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - // The event just before hot restart might never be received, - // but the injected client continues to work and send events - // after hot restart. - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - _hasKind( - EventKind.kServiceExtensionAdded, - ).having((e) => e.extensionRPC, 'service', 'ext.bar'), - ), - ); - - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - var isolate = await client.getIsolate(isolateId); - var library = isolate.rootLib!.uri!; - - final callback = '(_, __) async => ServiceExtensionResponse.result("")'; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.foo', $callback)", - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - isolate = await client.getIsolate(isolateId); - library = isolate.rootLib!.uri!; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.bar', $callback)", - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains('Hello World!')); - }); - - test('can refresh the page via the fullReload service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final fullReload = context.getRegisteredServiceExtension('fullReload'); - expect( - await fakeClient.callServiceExtension(fullReload!), - isA(), - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Should see only the new text - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - - test('can hot restart while paused', () async { - final client = context.debugConnection.vmService; - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - await client.streamListen('Debug'); - final stream = client.onEvent('Debug'); - final scriptList = await client.getScripts(isolateId); - final main = scriptList.scripts!.firstWhere( - (script) => script.uri!.contains('main.dart'), - ); - final bpLine = await context.findBreakpointLine( - 'printCount', - isolateId, - main, - ); - await client.addBreakpoint(isolateId, main.id!, bpLine); - await stream.firstWhere( - (event) => event.kind == EventKind.kPauseBreakpoint, - ); - - await makeEditAndWaitForRebuild(); - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - await fakeClient.callServiceExtension(hotRestart!); - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - - // Previous breakpoint should be cleared. - expect(isolate.breakpoints!.isEmpty, isTrue); - }); - - test('can evaluate expressions after hot restart', () async { - final client = context.debugConnection.vmService; - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - await fakeClient.callServiceExtension(hotRestart!); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - - // Expression evaluation while running should work. - final result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - }); - }, timeout: Timeout.factor(2)); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can hot restart changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. - expect( - source, - contains('start disassemble end disassemble $newString'), - ); - }); - - test('fires isolate create/destroy events during hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await makeEditAndWaitForRebuild(); - - await eventsDone; - }); - }); - - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('can hot restart changes ', () async { - await makeEditAndWaitForRebuild(); - - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. - expect( - source, - contains('start disassemble end disassemble $newString'), - ); - }); - }); - }, timeout: Timeout.factor(2)); - - // TODO(https://github.com/dart-lang/webdev/issues/2380): Run these tests with - // the FrontendServer as well. - group('when isolates_paused_on_start is true', () { - late VmService client; - late VmService fakeClient; - - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings(enableExpressionEvaluation: true), - ); - client = context.debugConnection.vmService; - fakeClient = await context.connectFakeClient(); - await client.setFlag('pause_isolates_on_start', 'true'); - await client.streamListen('Isolate'); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test( - 'after hot-restart, does not run app until there is a resume event', - () async { - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - final hotRestart = context.getRegisteredServiceExtension('hotRestart'); - expect( - await fakeClient.callServiceExtension(hotRestart!), - const TypeMatcher(), - ); - - await eventsDone; - - final sourceBeforeResume = await context.webDriver.pageSource; - expect(sourceBeforeResume.contains(newString), isFalse); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - await client.resume(isolateId); - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); - }, - ); - - test( - 'after page refresh, does not run app until there is a resume event', - () async { - await makeEditAndWaitForRebuild(); - - await context.webDriver.driver.refresh(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await eventsDone; - - final sourceBeforeResume = await context.webDriver.pageSource; - expect(sourceBeforeResume.contains(newString), isFalse); - - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - await client.resume(isolateId); - - final sourceAfterResume = await context.webDriver.pageSource; - expect(sourceAfterResume.contains(newString), isTrue); - }, - ); - }); -} - -TypeMatcher _hasKind(String kind) => - isA().having((e) => e.kind, 'kind', kind); diff --git a/frontend_server_common/lib/src/bootstrap.dart b/frontend_server_common/lib/src/bootstrap.dart index faf2fa420..b45ff45c2 100644 --- a/frontend_server_common/lib/src/bootstrap.dart +++ b/frontend_server_common/lib/src/bootstrap.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' show Platform; + // Note: this is a copy from flutter tools, updated to work with dwds tests /// JavaScript snippet to determine the base URL of the current path. @@ -393,6 +395,8 @@ $_baseUrlScript $_simpleLoaderScript (function() { + let appName = "org-dartlang-app:/$entrypoint"; + // Load pre-requisite DDC scripts. We intentionally use invalid names to avoid // namespace clashes. let prerequisiteScripts = [ @@ -413,26 +417,45 @@ $_simpleLoaderScript } Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); + // Save the current script so we can access it in a closure. + var _currentScript = document.currentScript; + + // Create a policy if needed to load the files during a hot restart. + let policy = { + createScriptURL: function(src) {return src;} + }; + if (self.trustedTypes && self.trustedTypes.createPolicy) { + policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); + } + var afterPrerequisiteLogic = function() { window.\$dartLoader.rootDirectories.push(_currentDirectory); let scripts = [ { "src": "dart_sdk.js", - "id": "dart_sdk \\0" + "id": "dart_sdk" }, + { + "src": "$bootstrapUrl", + "id": "data-main" + } ]; let loadConfig = new window.\$dartLoader.LoadConfiguration(); loadConfig.root = _currentDirectory; - loadConfig.bootstrapScript = { - "src": "$bootstrapUrl", - "id": "data-main" - }; - scripts.push(loadConfig.bootstrapScript); + + // TODO(srujzs): Verify this is sufficient for Windows. + loadConfig.isWindows = ${Platform.isWindows}; + loadConfig.bootstrapScript = scripts[scripts.length - 1]; + loadConfig.loadScriptFn = function(loader) { loader.addScriptsToQueue(scripts, null); loader.loadEnqueuedModules(); } + loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1; + loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2; + loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3; + let loader = new window.\$dartLoader.DDCLoader(loadConfig); // Record prerequisite scripts' fully resolved URLs. @@ -443,30 +466,118 @@ $_simpleLoaderScript window.\$dartLoader.loadConfig = loadConfig; window.\$dartLoader.loader = loader; - // TODO(srujzs): Support hot restart. - // Begin loading libraries loader.nextAttempt(); - } + + // Set up stack trace mapper. + if (window.\$dartStackTraceUtility && + !window.\$dartStackTraceUtility.ready) { + window.\$dartStackTraceUtility.ready = true; + window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { + var baseUrl = window.location.protocol + '//' + window.location.host; + url = url.replace(baseUrl + '/', ''); + if (url == 'dart_sdk.js') { + return dartDevEmbedder.debugger.getSourceMap('dart_sdk'); + } + url = url.replace(".lib.js", ""); + return dartDevEmbedder.debugger.getSourceMap(url); + }); + } + + let currentUri = _currentScript.src; + // We should have written a file containing all the scripts that need to be + // reloaded into the page. This is then read when a hot restart is triggered + // in DDC via the `\$dartReloadModifiedModules` callback. + let restartScripts = _currentDirectory + '/restart_scripts.json'; + + if (!window.\$dartReloadModifiedModules) { + window.\$dartReloadModifiedModules = (function(appName, callback) { + var xhttp = new XMLHttpRequest(); + xhttp.withCredentials = true; + xhttp.onreadystatechange = function() { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (this.readyState == 4 && this.status == 200 || this.status == 304) { + var scripts = JSON.parse(this.responseText); + var numToLoad = 0; + var numLoaded = 0; + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i]; + if (script.id == null) continue; + var src = _currentDirectory + '/' + script.src.toString(); + var oldSrc = window.\$dartLoader.moduleIdToUrl.get(script.id); + + // We might actually load from a different uri, delete the old one + // just to be sure. + window.\$dartLoader.urlToModuleId.delete(oldSrc); + + window.\$dartLoader.moduleIdToUrl.set(script.id, src); + window.\$dartLoader.urlToModuleId.set(src, script.id); + + numToLoad++; + + var el = document.getElementById(script.id); + if (el) el.remove(); + el = window.\$dartCreateScript(); + el.src = policy.createScriptURL(src); + el.async = false; + el.defer = true; + el.id = script.id; + el.onload = function() { + numLoaded++; + if (numToLoad == numLoaded) callback(); + }; + document.head.appendChild(el); + } + // Call `callback` right away if we found no updated scripts. + if (numToLoad == 0) callback(); + } + }; + xhttp.open("GET", restartScripts, true); + xhttp.send(); + }); + } + }; })(); '''; } -String generateDDCLibraryBundleMainModule({required String entrypoint}) { - return '''/* ENTRYPOINT_EXTENTION_MARKER */ +const String _onLoadEndCallback = r'$onLoadEndCallback'; + +String generateDDCLibraryBundleMainModule({ + required String entrypoint, + required String onLoadEndBootstrap, +}) { + // The typo below in "EXTENTION" is load-bearing, package:build depends on it. + return ''' +/* ENTRYPOINT_EXTENTION_MARKER */ (function() { let appName = "org-dartlang-app:///$entrypoint"; dartDevEmbedder.debugger.registerDevtoolsFormatter(); - let child = {}; - child.main = function() { - dartDevEmbedder.runMain(appName, {}); + // Set up a final script that lets us know when all scripts have been loaded. + // Only then can we call the main method. + let onLoadEndSrc = '$onLoadEndBootstrap'; + window.\$dartLoader.loadConfig.bootstrapScript = { + src: onLoadEndSrc, + id: onLoadEndSrc, + }; + window.\$dartLoader.loadConfig.tryLoadBootstrapScript = true; + // Should be called by $onLoadEndBootstrap once all the scripts have been + // loaded. + window.$_onLoadEndCallback = function() { + let child = {}; + child.main = function() { + dartDevEmbedder.runMain(appName, {}); + } + /* MAIN_EXTENSION_MARKER */ + child.main(); } - - /* MAIN_EXTENSION_MARKER */ - child.main(); })(); '''; } + +String generateDDCLibraryBundleOnLoadEndBootstrap() { + return '''window.$_onLoadEndCallback();'''; +} diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart index e7d5e67ba..2fb7bb96a 100644 --- a/frontend_server_common/lib/src/devfs.dart +++ b/frontend_server_common/lib/src/devfs.dart @@ -4,6 +4,9 @@ // Note: this is a copy from flutter tools, updated to work with dwds tests +import 'dart:convert'; +import 'dart:io'; + import 'package:dwds/asset_reader.dart'; import 'package:dwds/config.dart'; import 'package:dwds/expression_compiler.dart'; @@ -38,6 +41,8 @@ class WebDevFS { final PackageUriMapper packageUriMapper; final String index; final UrlEncoder? urlTunneler; + List sources = []; + DateTime? lastCompiled; @Deprecated('Only sound null safety is supported as of Dart 3.0') final bool soundNullSafety; @@ -73,109 +78,118 @@ class WebDevFS { required String dillOutputPath, required ResidentCompiler generator, required List invalidatedFiles, + required bool initialCompile, + required bool fullRestart, }) async { final mainPath = mainUri.toFilePath(); final outputDirectoryPath = fileSystem.file(mainPath).parent.path; final entryPoint = mainUri.toString(); - var ddcModuleLoader = 'ddc_module_loader.js'; - var require = 'require.js'; - var stackMapper = 'stack_trace_mapper.js'; - var main = 'main.dart.js'; - var bootstrap = 'main_module.bootstrap.js'; - + var prefix = ''; // If base path is not overwritten, use main's subdirectory // to store all files, so the paths match the requests. if (assetServer.basePath.isEmpty) { final directory = p.dirname(entryPoint); - ddcModuleLoader = '$directory/ddc_module_loader.js'; - require = '$directory/require.js'; - stackMapper = '$directory/stack_trace_mapper.js'; - main = '$directory/main.dart.js'; - bootstrap = '$directory/main_module.bootstrap.js'; + prefix = '$directory/'; } - assetServer.writeFile( - entryPoint, fileSystem.file(mainPath).readAsStringSync()); - assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync()); - - switch (ddcModuleFormat) { - case ModuleFormat.amd: - assetServer.writeFile(require, requireJS.readAsStringSync()); - assetServer.writeFile( - main, - generateBootstrapScript( - requireUrl: 'require.js', - mapperUrl: 'stack_trace_mapper.js', - entrypoint: entryPoint, - ), - ); - assetServer.writeFile( - bootstrap, - generateMainModule( - entrypoint: entryPoint, - ), - ); - break; - case ModuleFormat.ddc: - assetServer.writeFile( - ddcModuleLoader, ddcModuleLoaderJS.readAsStringSync()); - String bootstrapper; - String mainModule; - if (compilerOptions.canaryFeatures) { - bootstrapper = generateDDCLibraryBundleBootstrapScript( + if (initialCompile) { + final ddcModuleLoader = '${prefix}ddc_module_loader.js'; + final require = '${prefix}require.js'; + final stackMapper = '${prefix}stack_trace_mapper.js'; + final main = '${prefix}main.dart.js'; + final bootstrap = '${prefix}main_module.bootstrap.js'; + + assetServer.writeFile( + entryPoint, fileSystem.file(mainPath).readAsStringSync()); + assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync()); + + switch (ddcModuleFormat) { + case ModuleFormat.amd: + assetServer.writeFile(require, requireJS.readAsStringSync()); + assetServer.writeFile( + main, + generateBootstrapScript( + requireUrl: 'require.js', + mapperUrl: 'stack_trace_mapper.js', + entrypoint: entryPoint, + ), + ); + assetServer.writeFile( + bootstrap, + generateMainModule( + entrypoint: entryPoint, + ), + ); + break; + case ModuleFormat.ddc: + assetServer.writeFile( + ddcModuleLoader, ddcModuleLoaderJS.readAsStringSync()); + String bootstrapper; + String mainModule; + if (compilerOptions.canaryFeatures) { + bootstrapper = generateDDCLibraryBundleBootstrapScript( + ddcModuleLoaderUrl: ddcModuleLoader, + mapperUrl: stackMapper, + entrypoint: entryPoint, + bootstrapUrl: bootstrap); + const onLoadEndBootstrap = 'on_load_end_bootstrap.js'; + assetServer.writeFile(onLoadEndBootstrap, + generateDDCLibraryBundleOnLoadEndBootstrap()); + mainModule = generateDDCLibraryBundleMainModule( + entrypoint: entryPoint, onLoadEndBootstrap: onLoadEndBootstrap); + } else { + bootstrapper = generateDDCBootstrapScript( ddcModuleLoaderUrl: ddcModuleLoader, mapperUrl: stackMapper, entrypoint: entryPoint, - bootstrapUrl: bootstrap); - mainModule = - generateDDCLibraryBundleMainModule(entrypoint: entryPoint); - } else { - bootstrapper = generateDDCBootstrapScript( - ddcModuleLoaderUrl: ddcModuleLoader, - mapperUrl: stackMapper, - entrypoint: entryPoint, - bootstrapUrl: bootstrap, - ); + bootstrapUrl: bootstrap, + ); - // DDC uses a simple heuristic to determine exported identifier names. - // The module name (entrypoint name here) has its extension removed, - // and special path elements like '/', '\', and '..' are replaced with - // '__'. - final exportedMainName = pathToJSIdentifier(entryPoint.split('.')[0]); - mainModule = generateDDCMainModule( - entrypoint: entryPoint, exportedMain: exportedMainName); - } - assetServer.writeFile( - main, - bootstrapper, - ); - assetServer.writeFile( - bootstrap, - mainModule, - ); - break; - default: - throw Exception('Unsupported DDC module format $ddcModuleFormat.'); - } + // DDC uses a simple heuristic to determine exported identifier + // names. The module name (entrypoint name here) has its extension + // removed, and special path elements like '/', '\', and '..' are + // replaced with + // '__'. + final exportedMainName = + pathToJSIdentifier(entryPoint.split('.')[0]); + mainModule = generateDDCMainModule( + entrypoint: entryPoint, exportedMain: exportedMainName); + } + assetServer.writeFile( + main, + bootstrapper, + ); + assetServer.writeFile( + bootstrap, + mainModule, + ); + break; + default: + throw Exception('Unsupported DDC module format $ddcModuleFormat.'); + } - assetServer.writeFile('main_module.digests', '{}'); + assetServer.writeFile('main_module.digests', '{}'); - final sdk = dartSdk; - final sdkSourceMap = dartSdkSourcemap; - assetServer.writeFile('dart_sdk.js', sdk.readAsStringSync()); - assetServer.writeFile('dart_sdk.js.map', sdkSourceMap.readAsStringSync()); + final sdk = dartSdk; + final sdkSourceMap = dartSdkSourcemap; + assetServer.writeFile('dart_sdk.js', sdk.readAsStringSync()); + assetServer.writeFile('dart_sdk.js.map', sdkSourceMap.readAsStringSync()); + generator.reset(); + } - generator.reset(); final compilerOutput = await generator.recompile( Uri.parse('org-dartlang-app:///$mainUri'), invalidatedFiles, outputPath: p.join(dillOutputPath, 'app.dill'), packageConfig: packageUriMapper.packageConfig, + recompileRestart: fullRestart, ); if (compilerOutput == null || compilerOutput.errorCount > 0) { return UpdateFSReport(success: false); } + sources = compilerOutput.sources; + lastCompiled = DateTime.now(); File codeFile; File manifestFile; @@ -197,6 +211,16 @@ class WebDevFS { } on FileSystemException catch (err) { throw Exception('Failed to load recompiled sources:\n$err'); } + if (ddcModuleFormat == ModuleFormat.ddc && + compilerOptions.canaryFeatures && + !initialCompile) { + if (fullRestart) { + performRestart(modules); + } else { + // TODO(srujzs): Support hot reload testing. + throw Exception('Hot reload is not supported yet.'); + } + } return UpdateFSReport( success: true, syncedBytes: codeFile.lengthSync(), @@ -204,6 +228,27 @@ class WebDevFS { )..invalidatedModules = modules; } + /// Given a list of [modules] that need to be loaded, writes a list of sources + /// mapped to their ids to the file system that can then be consumed by the + /// hot restart callback. + /// + /// For example: + /// ```json + /// [ + /// { + /// "src": "", + /// "id": "", + /// }, + /// ] + /// ``` + void performRestart(List modules) { + final srcIdsList = >[]; + for (final src in modules) { + srcIdsList.add({'src': src, 'id': src}); + } + assetServer.writeFile('restart_scripts.json', json.encode(srcIdsList)); + } + File get ddcModuleLoaderJS => fileSystem.file(sdkLayout.ddcModuleLoaderJsPath); File get requireJS => fileSystem.file(sdkLayout.requireJsPath); @@ -244,3 +289,68 @@ class UpdateFSReport { /// Only used for JavaScript compilation. List? invalidatedModules; } + +/// The result of an invalidation check from [ProjectFileInvalidator]. +class InvalidationResult { + const InvalidationResult({this.uris}); + + final List? uris; +} + +/// The [ProjectFileInvalidator] track the dependencies for a running +/// application to determine when they are dirty. +class ProjectFileInvalidator { + ProjectFileInvalidator({required FileSystem fileSystem}) + : _fileSystem = fileSystem; + + final FileSystem _fileSystem; + + static const String _pubCachePathLinuxAndMac = '.pub-cache'; + static const String _pubCachePathWindows = 'Pub/Cache'; + + Future findInvalidated({ + required DateTime? lastCompiled, + required List urisToMonitor, + required String packagesPath, + }) async { + if (lastCompiled == null) { + // Initial load. + assert(urisToMonitor.isEmpty); + return InvalidationResult(uris: []); + } + + final urisToScan = [ + // Don't watch pub cache directories to speed things up a little. + for (final Uri uri in urisToMonitor) + if (_isNotInPubCache(uri)) uri, + ]; + final invalidatedFiles = []; + for (final uri in urisToScan) { + // Calling fs.statSync() is more performant than fs.file().statSync(), + // but uri.toFilePath() does not work with MultiRootFileSystem. + final updatedAt = uri.hasScheme && uri.scheme != 'file' + ? _fileSystem.file(uri).statSync().modified + : _fileSystem + .statSync(uri.toFilePath(windows: Platform.isWindows)) + .modified; + if (updatedAt.isAfter(lastCompiled)) { + invalidatedFiles.add(uri); + } + } + // We need to check the .dart_tool/package_config.json file too since it is + // not used in compilation. + final packageFile = _fileSystem.file(packagesPath); + final packageUri = packageFile.uri; + final updatedAt = packageFile.statSync().modified; + if (updatedAt.isAfter(lastCompiled)) { + invalidatedFiles.add(packageUri); + } + + return InvalidationResult(uris: invalidatedFiles); + } + + bool _isNotInPubCache(Uri uri) { + return !(Platform.isWindows && uri.path.contains(_pubCachePathWindows)) && + !uri.path.contains(_pubCachePathLinuxAndMac); + } +} diff --git a/frontend_server_common/lib/src/frontend_server_client.dart b/frontend_server_common/lib/src/frontend_server_client.dart index 9151cdda8..e500646b4 100644 --- a/frontend_server_common/lib/src/frontend_server_client.dart +++ b/frontend_server_common/lib/src/frontend_server_client.dart @@ -173,13 +173,15 @@ class _RecompileRequest extends _CompilationRequest { this.mainUri, this.invalidatedFiles, this.outputPath, - this.packageConfig, - ); + this.packageConfig, { + required this.recompileRestart, + }); Uri mainUri; List invalidatedFiles; String outputPath; PackageConfig packageConfig; + bool recompileRestart; @override Future _run(ResidentCompiler compiler) async => @@ -288,11 +290,14 @@ class ResidentCompiler { /// point that is used for recompilation. /// Binary file name is returned if compilation was successful, otherwise /// null is returned. + /// If [recompileRestart] is true, uses the `recompile-restart` instruction + /// instead of `recompile`. Future recompile( Uri mainUri, List invalidatedFiles, { required String outputPath, required PackageConfig packageConfig, + required bool recompileRestart, }) async { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); @@ -300,7 +305,8 @@ class ResidentCompiler { final completer = Completer(); _controller.add(_RecompileRequest( - completer, mainUri, invalidatedFiles, outputPath, packageConfig)); + completer, mainUri, invalidatedFiles, outputPath, packageConfig, + recompileRestart: recompileRestart)); return completer.future; } @@ -320,8 +326,10 @@ class ResidentCompiler { final server = _server!; final inputKey = generateV4UUID(); - server.stdin.writeln('recompile $mainUri$inputKey'); - _logger.info('<- recompile $mainUri$inputKey'); + final instruction = + request.recompileRestart ? 'recompile-restart' : 'recompile'; + server.stdin.writeln('$instruction $mainUri $inputKey'); + _logger.info('<- $instruction $mainUri $inputKey'); for (final fileUri in request.invalidatedFiles) { String message; if (fileUri.scheme == 'package') { diff --git a/frontend_server_common/lib/src/resident_runner.dart b/frontend_server_common/lib/src/resident_runner.dart index fafbf345a..604b33035 100644 --- a/frontend_server_common/lib/src/resident_runner.dart +++ b/frontend_server_common/lib/src/resident_runner.dart @@ -63,13 +63,21 @@ class ResidentWebRunner { late ResidentCompiler generator; late ExpressionCompiler expressionCompiler; - late WebDevFS devFS; - late Uri uri; + ProjectFileInvalidator? _projectFileInvalidator; + WebDevFS? devFS; + Uri? uri; late Iterable modules; Future run( - FileSystem fileSystem, String? hostname, int port, String index) async { - devFS = WebDevFS( + FileSystem fileSystem, + String? hostname, + int port, + String index, { + required bool initialCompile, + required bool fullRestart, + }) async { + _projectFileInvalidator ??= ProjectFileInvalidator(fileSystem: fileSystem); + devFS ??= WebDevFS( fileSystem: fileSystem, hostname: hostname ?? 'localhost', port: port, @@ -80,9 +88,10 @@ class ResidentWebRunner { sdkLayout: sdkLayout, compilerOptions: compilerOptions, ); - uri = await devFS.create(); + uri ??= await devFS!.create(); - final report = await _updateDevFS(); + final report = await _updateDevFS( + initialCompile: initialCompile, fullRestart: fullRestart); if (!report.success) { _logger.severe('Failed to compile application.'); return 1; @@ -94,17 +103,25 @@ class ResidentWebRunner { return 0; } - Future _updateDevFS() async { - final report = await devFS.update( + Future _updateDevFS( + {required bool initialCompile, required bool fullRestart}) async { + final invalidationResult = await _projectFileInvalidator!.findInvalidated( + lastCompiled: devFS!.lastCompiled, + urisToMonitor: devFS!.sources, + packagesPath: packageConfigFile.toFilePath(), + ); + final report = await devFS!.update( mainUri: mainUri, dillOutputPath: outputPath, generator: generator, - invalidatedFiles: []); + invalidatedFiles: invalidationResult.uris!, + initialCompile: initialCompile, + fullRestart: fullRestart); return report; } Future stop() async { await generator.shutdown(); - await devFS.dispose(); + await devFS!.dispose(); } }