From 408819b3f5fa85977e922e7ce895a8ff5d31ea5e Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 May 2026 17:55:12 -0400 Subject: [PATCH 01/15] [flutter_tools] Stop generating widget preview scaffold under $TMP Previously, the `widget_preview_scaffold` project was generated under the system temporary directory (`$TMP`) to work around issues with `flutter clean`. However, generating the scaffold in `$TMP` meant that the project was recreated on every run or could be lost, leading to unnecessary regeneration overhead and tracking issues. Move the generated `widget_preview_scaffold` project from `$TMP` to a `.widget_preview` directory located directly under the project root. To support this change, `WidgetPreviewGitignoreMigration` is introduced to automatically add `.widget_preview/` to the host project's `.gitignore`. In addition, `kProjectRootPath` is passed via DTD connection info to ensure `WidgetInspectorService` correctly adds the project root to pub root directories. Finally, file watchers (`LspPreviewDetector` and `PreviewDetector`) are updated to ignore the new `.widget_preview` directory. Fixes https://github.com/flutter/flutter/issues/179036 --- .../lib/src/dtd/dtd_connection_info.dart | 1 + .../src/widget_preview_inspector_service.dart | 2 + .../lib/src/commands/widget_preview.dart | 4 + .../widget_preview_gitignore_migration.dart | 42 ++++++++ packages/flutter_tools/lib/src/project.dart | 5 +- .../widget_preview/lsp_preview_detector.dart | 10 +- .../preview_code_generator.dart | 8 ++ .../src/widget_preview/preview_detector.dart | 3 +- .../lib/src/widget_preview/utils.dart | 1 + .../templates/app/.gitignore.tmpl | 4 + ...widget_preview_inspector_service.dart.tmpl | 2 + .../preview_code_generator_test.dart | 3 + ...dget_preview_gitignore_migration_test.dart | 95 +++++++++++++++++++ .../widget_preview_smoke_test.dart | 83 +++++++--------- 14 files changed, 210 insertions(+), 53 deletions(-) create mode 100644 packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart create mode 100644 packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart diff --git a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart index def881bd73a03..75ee834cc65d1 100644 --- a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart +++ b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart @@ -5,3 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; +const String kProjectRootPath = ''; diff --git a/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart b/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart index d91e39e752138..224fb37570e54 100644 --- a/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart +++ b/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; @@ -16,6 +17,7 @@ import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories([kProjectRootPath]); } /// The DTD services instance used to communicate with the tool. diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index ccc79c68f0991..113923544c1cc 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -28,6 +28,7 @@ import '../device.dart'; import '../features.dart'; import '../globals.dart' as globals; import '../isolated/resident_web_runner.dart'; +import '../migrations/widget_preview_gitignore_migration.dart'; import '../project.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; @@ -322,6 +323,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C previewAnalytics.initializeLaunchStopwatch(); logger.sendInitializingEvent(); + await WidgetPreviewGitignoreMigration(rootProject, logger).migrate(); + final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir); widgetPreviewScaffold = customPreviewScaffoldOutput != null ? fs.directory(customPreviewScaffoldOutput) @@ -414,6 +417,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C dtdUri: _dtdService.dtdUri!, widgetPreviewServiceName: _dtdService.widgetPreviewService, widgetPreviewScaffoldStreamName: _dtdService.widgetPreviewScaffoldStream, + projectRootPath: rootProject.directory.absolute.path, ); final FlutterWidgetPreviews originalPreviews = await _dtdService.getFlutterWidgetPreviews(); diff --git a/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart new file mode 100644 index 0000000000000..54bde8db1c898 --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart @@ -0,0 +1,42 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/file_system.dart'; +import '../base/project_migrator.dart'; +import '../project.dart'; + +/// Adds `.widget_preview/` to the .gitignore file. +class WidgetPreviewGitignoreMigration extends ProjectMigrator { + WidgetPreviewGitignoreMigration(FlutterProject project, super.logger) + : _gitignoreFile = project.gitignoreFile; + + final File _gitignoreFile; + + @override + Future migrate() async { + if (!_gitignoreFile.existsSync()) { + logger.printTrace('.gitignore file not found, skipping widget preview .gitignore migration.'); + return; + } + + final String originalContent = _gitignoreFile.readAsStringSync(); + + // Skip if .gitignore is already migrated. + if (originalContent.contains('.widget_preview/')) { + return; + } + + logger.printWarning('.gitignore does not ignore .widget_preview/ directory, updating.'); + + final newContent = StringBuffer(originalContent); + if (!originalContent.endsWith('\n')) { + newContent.writeln(); + } + newContent + ..writeln('# Widget Preview related') + ..writeln('.widget_preview/'); + + _gitignoreFile.writeAsStringSync(newContent.toString()); + } +} diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 042444cd94185..54471a8f15315 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -233,10 +233,7 @@ class FlutterProject { /// The location of the generated scaffolding project for hosting widget /// previews from this project. - // TODO(bkonyi): don't create this project in $TMP. - // See https://github.com/flutter/flutter/issues/179036 - late final Directory widgetPreviewScaffold = directory.fileSystem.systemTempDirectory - .createTempSync('widget_preview_scaffold'); + late final Directory widgetPreviewScaffold = directory.childDirectory('.widget_preview'); /// The directory containing the generated code for this project. Directory get generated => directory.absolute diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index 4b75c144d0cea..ab0d7f6e74d12 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -168,7 +168,15 @@ class LspPreviewDetector { // Only process one FileSystemEntity at a time so we don't invalidate an AnalysisSession that's // in use when we call context.changeFile(...). await mutex.runGuarded(() async { - await _fileAddedOrUpdated(filePath: event.path); + final String eventPath = event.path; + // Ignore any files under .dart_tool, .widget_preview, or ephemeral directories created by + // the tool (e.g., build/, plugin directories, etc.). + if (eventPath.doesContainDartTool || + eventPath.doesContainWidgetPreview || + project.ephemeralDirectories.any((dir) => eventPath.contains(dir.path))) { + return; + } + await _fileAddedOrUpdated(filePath: eventPath); }); } diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart index 23f7308c2de7f..11b97e6589991 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart @@ -60,6 +60,7 @@ class PreviewCodeGenerator { required Uri dtdUri, required String widgetPreviewServiceName, required String widgetPreviewScaffoldStreamName, + required String projectRootPath, }) { final emitter = cb.DartEmitter.scoped(useNullSafetySyntax: true); final lib = cb.Library( @@ -87,6 +88,13 @@ class PreviewCodeGenerator { ..type = cb.refer('String') ..assignment = cb.literalString(widgetPreviewScaffoldStreamName).code; }), + cb.Field((b) { + b + ..name = 'kProjectRootPath' + ..modifier = cb.FieldModifier.constant + ..type = cb.refer('String') + ..assignment = cb.literalString(projectRootPath).code; + }), ]), ); final File generatedDtdConnectionInfoFile = fs.file( diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart index eb0ff03eda499..72d6df081962e 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart @@ -154,9 +154,10 @@ class PreviewDetector { onPackageConfigChangeDetected?.call(event.path); return; } - // Ignore any files under .dart_tool or ephemeral directories created by + // Ignore any files under .dart_tool, .widget_preview, or ephemeral directories created by // the tool (e.g., build/, plugin directories, etc.). if (eventPath.doesContainDartTool || + eventPath.doesContainWidgetPreview || project.ephemeralDirectories.any((dir) => eventPath.contains(dir.path))) { return; } diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index 60fe3398b4795..675a4659d2bea 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -64,6 +64,7 @@ extension StringExtension on String { bool get isDartFile => endsWith('.dart'); bool get isPubspec => endsWith('pubspec.yaml'); bool get doesContainDartTool => contains('.dart_tool'); + bool get doesContainWidgetPreview => contains('.widget_preview'); } extension LibraryElementExtension on LibraryElement { diff --git a/packages/flutter_tools/templates/app/.gitignore.tmpl b/packages/flutter_tools/templates/app/.gitignore.tmpl index 3820a95c65c3e..c7ada08ac10c1 100644 --- a/packages/flutter_tools/templates/app/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app/.gitignore.tmpl @@ -43,3 +43,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Widget Preview related +.widget_preview/* +!.widget_preview/bin/* diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl index d91e39e752138..224fb37570e54 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; @@ -16,6 +17,7 @@ import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories([kProjectRootPath]); } /// The DTD services instance used to communicate with the tool. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart index d8f5fc422e64f..520dc770bc6b7 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart @@ -463,6 +463,7 @@ List<_i1.WidgetPreview> previews() => []; dtdUri: dtdUri, widgetPreviewServiceName: 'widget-preview-service', widgetPreviewScaffoldStreamName: 'widget-preview-stream', + projectRootPath: project.directory.absolute.path, ); final expectedDtdConnectionInfo = @@ -472,6 +473,8 @@ List<_i1.WidgetPreview> previews() => []; const String kWidgetPreviewDtdUri = '$dtdUri'; const String kWidgetPreviewService = 'widget-preview-service'; const String kWidgetPreviewScaffoldStream = 'widget-preview-stream'; +const String kProjectRootPath = + '${project.directory.absolute.path}'; '''; expect(generatedDtdConnectionInfoFile.readAsStringSync(), expectedDtdConnectionInfo); }, diff --git a/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart new file mode 100644 index 0000000000000..aecbf6bcbc0cc --- /dev/null +++ b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart @@ -0,0 +1,95 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/migrations/widget_preview_gitignore_migration.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +void main() { + group('Widget Preview .gitignore migration', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeFlutterProject mockProject; + late File gitignoreFile; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + gitignoreFile = memoryFileSystem.file('.gitignore'); + + testLogger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences.test(), + ); + + mockProject = FakeFlutterProject(fileSystem: memoryFileSystem); + }); + + testWithoutContext('skipped if .gitignore file is missing', () async { + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + expect(gitignoreFile.existsSync(), isFalse); + + expect( + testLogger.traceText, + contains('.gitignore file not found, skipping widget preview .gitignore migration.'), + ); + expect(testLogger.warningText, isEmpty); + }); + + testWithoutContext('skipped if already migrated', () async { + const gitignoreFileContents = ''' +.DS_Store +.widget_preview/ +'''; + + gitignoreFile.writeAsStringSync(gitignoreFileContents); + + final DateTime updatedAt = gitignoreFile.lastModifiedSync(); + + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + + expect(gitignoreFile.lastModifiedSync(), updatedAt); + expect(gitignoreFile.readAsStringSync(), gitignoreFileContents); + expect(testLogger.warningText, isEmpty); + }); + + testWithoutContext('migrates project to ignore .widget_preview directory', () async { + gitignoreFile.writeAsStringSync( + '.DS_Store\n' + '.atom/\n', + ); + + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + + expect( + gitignoreFile.readAsStringSync(), + '.DS_Store\n' + '.atom/\n' + '# Widget Preview related\n' + '.widget_preview/\n', + ); + + expect( + testLogger.warningText, + contains('.gitignore does not ignore .widget_preview/ directory, updating.'), + ); + }); + }); +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({required MemoryFileSystem fileSystem}) + : gitignoreFile = fileSystem.file('.gitignore'); + + @override + File gitignoreFile; +} diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index ea16c7fd33e37..e4d55f96cc91c 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -56,53 +56,42 @@ void main() { ); }); - testWithoutContext( - 'runs flutter pub get in widget_preview_scaffold if ' - "widget_preview_scaffold/.dart_tool doesn't exist", - () async { - // Regression test for https://github.com/flutter/flutter/issues/178660 - // Generate the widget preview scaffold, but don't bother launching it. - processManager.runSync([ - flutterBin, - 'widget-preview', - 'start', - '--no-${WidgetPreviewStartCommand.kLaunchPreviewer}', - ], workingDirectory: tempDir.path); - - // Ensure widget_preview_scaffold/.dart_tool/package_config.json exists. - final Directory widgetPreviewScaffoldDartTool = tempDir - .childDirectory('.dart_tool') - .childDirectory('widget_preview_scaffold') - .childDirectory('.dart_tool'); - expect(widgetPreviewScaffoldDartTool, exists); - expect(widgetPreviewScaffoldDartTool.childFile('package_config.json'), exists); - - // Delete widget_preview_scaffold/.dart_tool/. This simulates an interrupted - // flutter widget-preview start where 'flutter pub get' wasn't run after - // the widget_preview_scaffold project was created. - widgetPreviewScaffoldDartTool.deleteSync(recursive: true); - - // Ensure we don't crash due to the package_config.json lookup pointing to - // the parent project's package_config.json due to - // widget_preview_scaffold/.dart_tool/package_config.json not existing. - await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); - }, - // Project is always regenerated. - skip: true, // See https://github.com/flutter/flutter/issues/179036. - ); - - testWithoutContext( - 'does not recreate project on subsequent runs', - () async { - // The first run of 'flutter widget-preview start' should generate a new preview scaffold - await runWidgetPreview(tempDir: tempDir, expectedMessages: firstLaunchMessagesWeb); - - // We shouldn't regenerate the scaffold after the initial run. - await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); - }, - // Project is always regenerated. - skip: true, // See https://github.com/flutter/flutter/issues/179036. - ); + testWithoutContext('runs flutter pub get in widget_preview_scaffold if ' + "widget_preview_scaffold/.dart_tool doesn't exist", () async { + // Regression test for https://github.com/flutter/flutter/issues/178660 + // Generate the widget preview scaffold, but don't bother launching it. + processManager.runSync([ + flutterBin, + 'widget-preview', + 'start', + '--no-${WidgetPreviewStartCommand.kLaunchPreviewer}', + ], workingDirectory: tempDir.path); + + // Ensure .widget_preview/.dart_tool/package_config.json exists. + final Directory widgetPreviewScaffoldDartTool = tempDir + .childDirectory('.widget_preview') + .childDirectory('.dart_tool'); + expect(widgetPreviewScaffoldDartTool, exists); + expect(widgetPreviewScaffoldDartTool.childFile('package_config.json'), exists); + + // Delete .widget_preview/.dart_tool/. This simulates an interrupted + // flutter widget-preview start where 'flutter pub get' wasn't run after + // the widget_preview_scaffold project was created. + widgetPreviewScaffoldDartTool.deleteSync(recursive: true); + + // Ensure we don't crash due to the package_config.json lookup pointing to + // the parent project's package_config.json due to + // .widget_preview/.dart_tool/package_config.json not existing. + await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); + }); + + testWithoutContext('does not recreate project on subsequent runs', () async { + // The first run of 'flutter widget-preview start' should generate a new preview scaffold + await runWidgetPreview(tempDir: tempDir, expectedMessages: firstLaunchMessagesWeb); + + // We shouldn't regenerate the scaffold after the initial run. + await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); + }); testUsingContext('can connect to an existing DTD instance', () async { dtdLauncher = DtdLauncher( From 939a5cdb586637dc66382830e0e48d444299bfe6 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 May 2026 18:05:19 -0400 Subject: [PATCH 02/15] [flutter_tools] Simplify .widget_preview ignore pattern in template The template previously ignored `.widget_preview/*` but explicitly allowed `!.widget_preview/bin/*`. Since `.widget_preview` is a generated, ephemeral scaffold project for hosting widget previews, tracking any part of it in the host project repository is unnecessary and inconsistent with the migrator logic. Update `packages/flutter_tools/templates/app/.gitignore.tmpl` to ignore the entire `.widget_preview/` directory, aligning it with `WidgetPreviewGitignoreMigration`. Fixes https://github.com/flutter/flutter/issues/179036 --- packages/flutter_tools/templates/app/.gitignore.tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/flutter_tools/templates/app/.gitignore.tmpl b/packages/flutter_tools/templates/app/.gitignore.tmpl index c7ada08ac10c1..79f7ecabd5c79 100644 --- a/packages/flutter_tools/templates/app/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app/.gitignore.tmpl @@ -45,5 +45,4 @@ app.*.map.json /android/app/release # Widget Preview related -.widget_preview/* -!.widget_preview/bin/* +.widget_preview/ From 559b863322acef576c500747e70e92e2ba756697 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 May 2026 20:39:29 -0400 Subject: [PATCH 03/15] [flutter_tools] Address review feedback for widget preview - Make `.widget_preview` check in `utils.dart` more specific by checking path segments to avoid false positives. - Lower logging level in `WidgetPreviewGitignoreMigration` from warning to trace. - Update tests to expect trace output. Fixes https://github.com/flutter/flutter/issues/179036 --- .../lib/src/migrations/widget_preview_gitignore_migration.dart | 2 +- packages/flutter_tools/lib/src/widget_preview/utils.dart | 2 +- .../migrations/widget_preview_gitignore_migration_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart index 54bde8db1c898..ce664328c0809 100644 --- a/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart +++ b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart @@ -27,7 +27,7 @@ class WidgetPreviewGitignoreMigration extends ProjectMigrator { return; } - logger.printWarning('.gitignore does not ignore .widget_preview/ directory, updating.'); + logger.printTrace('.gitignore does not ignore .widget_preview/ directory, updating.'); final newContent = StringBuffer(originalContent); if (!originalContent.endsWith('\n')) { diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index 675a4659d2bea..5e2ffab90bad6 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -64,7 +64,7 @@ extension StringExtension on String { bool get isDartFile => endsWith('.dart'); bool get isPubspec => endsWith('pubspec.yaml'); bool get doesContainDartTool => contains('.dart_tool'); - bool get doesContainWidgetPreview => contains('.widget_preview'); + bool get doesContainWidgetPreview => split(RegExp(r'[/\\]')).contains('.widget_preview'); } extension LibraryElementExtension on LibraryElement { diff --git a/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart index aecbf6bcbc0cc..7fd065c3e99d2 100644 --- a/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart @@ -79,7 +79,7 @@ void main() { ); expect( - testLogger.warningText, + testLogger.traceText, contains('.gitignore does not ignore .widget_preview/ directory, updating.'), ); }); From 5d915f5728883c2a9344bc1711038fe4dd2b1d8f Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Thu, 14 May 2026 10:53:41 -0400 Subject: [PATCH 04/15] [flutter_tools] Add kProjectRootPath to dtd_connection_info template The template `widget_preview_inspector_service.dart.tmpl` references `kProjectRootPath`, but `dtd_connection_info.dart.tmpl` was missing its definition. This caused analysis failures when the template or dummy project was analyzed. Added `const String kProjectRootPath = '';` to the template to resolve the error. Fixes https://github.com/flutter/flutter/issues/179036 --- .../lib/src/dtd/dtd_connection_info.dart.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl index def881bd73a03..75ee834cc65d1 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl @@ -5,3 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; +const String kProjectRootPath = ''; From 31499598436a26d331d0bbbc040fec44599757ff Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 15 May 2026 12:41:28 -0400 Subject: [PATCH 05/15] [flutter_tools] Generate kProjectRootPath as a raw string Updates kProjectRootPath generation in PreviewCodeGenerator to produce a raw string (r'...'), ensuring that absolute file paths (especially on Windows) are correctly represented without escape sequence errors. Also updates the initial constant definition in the scaffold template and inflated file to be a raw string. Fixes https://github.com/flutter/flutter/issues/179036 --- .../lib/src/dtd/dtd_connection_info.dart | 2 +- .../lib/src/widget_preview/preview_code_generator.dart | 2 +- .../lib/src/dtd/dtd_connection_info.dart.tmpl | 2 +- .../hermetic/widget_preview/preview_code_generator_test.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart index 75ee834cc65d1..1d8cacc43d3ee 100644 --- a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart +++ b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart @@ -5,4 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; -const String kProjectRootPath = ''; +const String kProjectRootPath = r''; diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart index 11b97e6589991..d37eec27b13d2 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart @@ -93,7 +93,7 @@ class PreviewCodeGenerator { ..name = 'kProjectRootPath' ..modifier = cb.FieldModifier.constant ..type = cb.refer('String') - ..assignment = cb.literalString(projectRootPath).code; + ..assignment = cb.literalString(projectRootPath, raw: true).code; }), ]), ); diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl index 75ee834cc65d1..1d8cacc43d3ee 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl @@ -5,4 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; -const String kProjectRootPath = ''; +const String kProjectRootPath = r''; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart index 520dc770bc6b7..b5b3f64dd19f9 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart @@ -474,7 +474,7 @@ const String kWidgetPreviewDtdUri = '$dtdUri'; const String kWidgetPreviewService = 'widget-preview-service'; const String kWidgetPreviewScaffoldStream = 'widget-preview-stream'; const String kProjectRootPath = - '${project.directory.absolute.path}'; + r'${project.directory.absolute.path}'; '''; expect(generatedDtdConnectionInfoFile.readAsStringSync(), expectedDtdConnectionInfo); }, From 953a746c8af9ff675acb49ac5c3c2d5efb7c044a Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 19 May 2026 14:48:36 -0400 Subject: [PATCH 06/15] Increase delay for waiting for analysis to fix tests --- .../lib/src/widget_preview/lsp_preview_detector.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index ab0d7f6e74d12..60b434e1d8fd1 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -185,7 +185,7 @@ class LspPreviewDetector { onPubspecChangeDetected(filePath); return; } - await _analysisServer?.waitForAnalysis(); + await _analysisServer?.waitForAnalysis(delay: const Duration(seconds: 5)); try { final FlutterWidgetPreviews result = await dtd.getFlutterWidgetPreviews(); onChangeDetected(result); From 43dc1084faa512ecc1fdea44dd64c4b622f2022b Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 19 May 2026 17:04:53 -0400 Subject: [PATCH 07/15] Attempt to fix Windows failures --- .../test/integration.shard/test_data/basic_project.dart | 7 +++++-- .../test/integration.shard/test_data/project.dart | 8 +++++--- .../integration.shard/widget_preview_detection_test.dart | 5 ++++- .../integration.shard/widget_preview_pubspec_test.dart | 5 ++++- .../test/integration.shard/widget_preview_smoke_test.dart | 5 ++++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart index fd0afb93792cd..8bb8583c01ff2 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart @@ -5,9 +5,12 @@ import 'project.dart'; class BasicProject extends Project { + BasicProject({super.name}); + @override - final pubspec = ''' - name: test + String get pubspec => + ''' + name: $name environment: sdk: ^3.7.0-0 diff --git a/packages/flutter_tools/test/integration.shard/test_data/project.dart b/packages/flutter_tools/test/integration.shard/test_data/project.dart index 44d2743f332e0..993558604dbbe 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/project.dart @@ -27,7 +27,9 @@ abstract class Project { /// Creates a flutter Project for testing. /// /// If passed, `indexHtml` is used as the contents of the web/index.html file. - Project({this.indexHtml = _kDefaultHtml}); + Project({this.indexHtml = _kDefaultHtml, String? name}) : name = name ?? 'test'; + + final String name; late Directory dir; @@ -37,7 +39,7 @@ abstract class Project { String get generatedFile => ''; DeferredComponentsConfig? get deferredComponents => null; - Uri get mainDart => Uri.parse('package:test/main.dart'); + Uri get mainDart => Uri.parse('package:$name/main.dart'); /// The contents for the index.html file of this `Project`. /// @@ -67,7 +69,7 @@ abstract class Project { writeFile(fileSystem.path.join(dir.path, 'web', 'index.html'), indexHtml); writeFile(fileSystem.path.join(dir.path, 'web', 'flutter.js'), ''); writeFile(fileSystem.path.join(dir.path, 'web', 'flutter_service_worker.js'), ''); - writePackageConfigFiles(directory: dir, mainLibName: 'test'); + writePackageConfigFiles(directory: dir, mainLibName: name); await getPackages(dir.path); } diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart index 0f3f57fa90ea2..64243b220e89a 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart @@ -21,11 +21,14 @@ void main() { late Directory tempDir; Logger? logger; DtdLauncher? dtdLauncher; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; setUp(() async { logger = BufferLogger.test(); tempDir = createResolvedTempDirectorySync('widget_preview_detection_test.'); + projectCounter++; + project = BasicProject(name: 'test_detection_$projectCounter'); await project.setUpIn(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart index 2717cf9944d9d..50b51ab240fe2 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart @@ -14,10 +14,13 @@ import 'widget_preview_test_helpers.dart'; void main() { late Directory tempDir; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; setUp(() async { tempDir = createResolvedTempDirectorySync('widget_preview_pubspec_test.'); + projectCounter++; + project = BasicProject(name: 'test_pubspec_$projectCounter'); await project.setUpIn(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index e4d55f96cc91c..76af3eacd9a9f 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -26,12 +26,15 @@ void main() { Logger? logger; DtdLauncher? dtdLauncher; DevtoolsServerLauncher? devtoolsLauncher; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; const ProcessManager processManager = LocalProcessManager(); setUp(() async { logger = BufferLogger.test(); tempDir = createResolvedTempDirectorySync('widget_preview_smoke_test.'); + projectCounter++; + project = BasicProject(name: 'test_smoke_$projectCounter'); await project.setUpIn(tempDir); }); From 784c5f513c5c93d9299277c37d25920ecb8238ea Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 19 May 2026 18:25:02 -0400 Subject: [PATCH 08/15] Fix tests? --- .../widget_preview_test_helpers.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index 9f505a7660c5a..bbf26c7453934 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -60,8 +60,19 @@ Future> startWidgetPreview({ ], workingDirectory: tempDir.path); addTearDown(() async { - process.kill(); - await process.exitCode; + try { + process.stdin.writeln('q'); + await process.exitCode.timeout( + const Duration(seconds: 5), + onTimeout: () { + process.kill(); + return process.exitCode; + }, + ); + } on Object catch (_) { + process.kill(); + await process.exitCode; + } }); final controller = StreamController.broadcast(); From 8782d46443437c98cf7735a1774e253e19e1c253 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 19 May 2026 19:59:29 -0400 Subject: [PATCH 09/15] Cleanly terminate leaked Analysis Server processes on Windows in tearDown --- .../test/integration.shard/test_utils.dart | 14 ++++++++++++++ .../widget_preview_detection_test.dart | 1 + .../widget_preview_pubspec_test.dart | 1 + .../widget_preview_smoke_test.dart | 1 + 4 files changed, 17 insertions(+) diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index b004394619a4d..5ca43749ba6bd 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -63,6 +63,20 @@ Future getPackages(String folder) async { } } +void killLeakedAnalysisServers() { + if (platform.isWindows) { + try { + processManager.runSync([ + 'powershell', + '-Command', + 'Get-CimInstance Win32_Process -Filter "CommandLine like \'%language-server%\'" | ForEach-Object { \$_.Terminate() }', + ]); + } on Object catch (_) { + // Ignore any errors during process termination. + } + } +} + const kLocalEngineEnvironment = 'FLUTTER_LOCAL_ENGINE'; const kLocalEngineHostEnvironment = 'FLUTTER_LOCAL_ENGINE_HOST'; const kLocalEngineLocation = 'FLUTTER_LOCAL_ENGINE_SRC_PATH'; diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart index 64243b220e89a..8fb60cc9c98ea 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart @@ -35,6 +35,7 @@ void main() { tearDown(() async { await dtdLauncher?.dispose(); dtdLauncher = null; + killLeakedAnalysisServers(); tryToDelete(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart index 50b51ab240fe2..1762ed18ed910 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart @@ -25,6 +25,7 @@ void main() { }); tearDown(() async { + killLeakedAnalysisServers(); tryToDelete(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index 76af3eacd9a9f..69665eeb2ffbc 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -43,6 +43,7 @@ void main() { await devtoolsLauncher?.close(); devtoolsLauncher = null; dtdLauncher = null; + killLeakedAnalysisServers(); tryToDelete(tempDir); }); From f69a20827b088444f3159840a0854c59adce4d61 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 20 May 2026 09:54:27 -0400 Subject: [PATCH 10/15] Refactor: surgically kill child processes recursively on Windows in startWidgetPreview --- packages/flutter_tools/test/integration.shard/test_utils.dart | 4 ++-- .../test/integration.shard/widget_preview_detection_test.dart | 1 - .../test/integration.shard/widget_preview_pubspec_test.dart | 1 - .../test/integration.shard/widget_preview_smoke_test.dart | 1 - .../test/integration.shard/widget_preview_test_helpers.dart | 2 ++ 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index 5ca43749ba6bd..ecfa95f807339 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -63,13 +63,13 @@ Future getPackages(String folder) async { } } -void killLeakedAnalysisServers() { +void killChildProcesses(int parentPid) { if (platform.isWindows) { try { processManager.runSync([ 'powershell', '-Command', - 'Get-CimInstance Win32_Process -Filter "CommandLine like \'%language-server%\'" | ForEach-Object { \$_.Terminate() }', + 'function Kill-Tree(\$id) { Get-CimInstance Win32_Process -Filter "ParentProcessId = \$id" | ForEach-Object { Kill-Tree \$_.ProcessId; \$_.Terminate() } }; Kill-Tree $parentPid', ]); } on Object catch (_) { // Ignore any errors during process termination. diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart index 8fb60cc9c98ea..64243b220e89a 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart @@ -35,7 +35,6 @@ void main() { tearDown(() async { await dtdLauncher?.dispose(); dtdLauncher = null; - killLeakedAnalysisServers(); tryToDelete(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart index 1762ed18ed910..50b51ab240fe2 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart @@ -25,7 +25,6 @@ void main() { }); tearDown(() async { - killLeakedAnalysisServers(); tryToDelete(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index 69665eeb2ffbc..76af3eacd9a9f 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -43,7 +43,6 @@ void main() { await devtoolsLauncher?.close(); devtoolsLauncher = null; dtdLauncher = null; - killLeakedAnalysisServers(); tryToDelete(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index bbf26c7453934..7fc417c884f3a 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -72,6 +72,8 @@ Future> startWidgetPreview({ } on Object catch (_) { process.kill(); await process.exitCode; + } finally { + killChildProcesses(process.pid); } }); From 0bdd68d6dfb94049079f56a47c6134a76257a15e Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 20 May 2026 09:55:59 -0400 Subject: [PATCH 11/15] Cleanup: remove stdin write 'q' and timeout from startWidgetPreview addTearDown --- .../integration.shard/widget_preview_test_helpers.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index 7fc417c884f3a..82c0da221eeae 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -61,15 +61,6 @@ Future> startWidgetPreview({ addTearDown(() async { try { - process.stdin.writeln('q'); - await process.exitCode.timeout( - const Duration(seconds: 5), - onTimeout: () { - process.kill(); - return process.exitCode; - }, - ); - } on Object catch (_) { process.kill(); await process.exitCode; } finally { From 82481bc1a13609098509a80d0b851f335e496041 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 20 May 2026 09:56:43 -0400 Subject: [PATCH 12/15] Doc: Add documentation for killChildProcesses explaining Windows parent process termination limitations --- .../test/integration.shard/test_utils.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index ecfa95f807339..a7c3598ee2908 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -63,6 +63,19 @@ Future getPackages(String folder) async { } } +/// Surgically and recursively terminates all descendant processes spawned by [parentPid] on Windows. +/// +/// This is necessary on Windows because `Process.kill()` maps to `TerminateProcess`, +/// which instantly terminates the parent process without executing its registered +/// shutdown/cleanup hooks or propagating termination to its child processes. +/// +/// As a result, grandchild processes (like the spawned `dart language-server`) +/// get orphaned, keep running in the background, and maintain active directory/file locks +/// on the temp directories, causing subsequent tests to fail on `PathAccessException` and +/// semantic analysis errors during directory deletion in `tearDown`. +/// +/// This helper recursively kills the entire process tree starting from [parentPid] to ensure +/// zero leaked processes and file locks. void killChildProcesses(int parentPid) { if (platform.isWindows) { try { From ba25e9abc5f8cf151d47798729db0d2f61989392 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 20 May 2026 10:57:16 -0400 Subject: [PATCH 13/15] Fix: Use taskkill /F /T to recursively and surgically kill process trees on Windows --- .../test/integration.shard/test_utils.dart | 27 ------------------- .../widget_preview_test_helpers.dart | 11 +++++--- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index a7c3598ee2908..b004394619a4d 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -63,33 +63,6 @@ Future getPackages(String folder) async { } } -/// Surgically and recursively terminates all descendant processes spawned by [parentPid] on Windows. -/// -/// This is necessary on Windows because `Process.kill()` maps to `TerminateProcess`, -/// which instantly terminates the parent process without executing its registered -/// shutdown/cleanup hooks or propagating termination to its child processes. -/// -/// As a result, grandchild processes (like the spawned `dart language-server`) -/// get orphaned, keep running in the background, and maintain active directory/file locks -/// on the temp directories, causing subsequent tests to fail on `PathAccessException` and -/// semantic analysis errors during directory deletion in `tearDown`. -/// -/// This helper recursively kills the entire process tree starting from [parentPid] to ensure -/// zero leaked processes and file locks. -void killChildProcesses(int parentPid) { - if (platform.isWindows) { - try { - processManager.runSync([ - 'powershell', - '-Command', - 'function Kill-Tree(\$id) { Get-CimInstance Win32_Process -Filter "ParentProcessId = \$id" | ForEach-Object { Kill-Tree \$_.ProcessId; \$_.Terminate() } }; Kill-Tree $parentPid', - ]); - } on Object catch (_) { - // Ignore any errors during process termination. - } - } -} - const kLocalEngineEnvironment = 'FLUTTER_LOCAL_ENGINE'; const kLocalEngineHostEnvironment = 'FLUTTER_LOCAL_ENGINE_HOST'; const kLocalEngineLocation = 'FLUTTER_LOCAL_ENGINE_SRC_PATH'; diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index 82c0da221eeae..ff475d54d20e4 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -60,11 +60,16 @@ Future> startWidgetPreview({ ], workingDirectory: tempDir.path); addTearDown(() async { - try { + if (platform.isWindows) { + try { + processManager.runSync(['taskkill', '/F', '/T', '/PID', '${process.pid}']); + } on Object catch (_) { + process.kill(); + } + await process.exitCode; + } else { process.kill(); await process.exitCode; - } finally { - killChildProcesses(process.pid); } }); From f0bf4254965dfdd732c71f14d06dcdce93a0b93f Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 20 May 2026 11:28:35 -0400 Subject: [PATCH 14/15] Fix: Add getPreviews helper with Windows-specific delay to avoid analysis race conditions --- .../widget_preview_detection_test.dart | 24 +++++-------------- .../widget_preview_test_helpers.dart | 10 ++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart index 64243b220e89a..cef80d3cd0732 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart @@ -77,10 +77,7 @@ Widget myNewPreview() => Container(); ); await reloadSub.cancel(); - final DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + final DTDResponse result = await getPreviews(dtdConnection); final FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -128,10 +125,7 @@ Widget myRemovePreview() => Container(); ); await initReloadSub.cancel(); - DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + DTDResponse result = await getPreviews(dtdConnection); FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -154,7 +148,7 @@ Widget myRemovePreview() => Container(); ); await deleteReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map); if (previews.previews.isNotEmpty) { throw StateError('Preview was still detected after deletion!'); @@ -200,10 +194,7 @@ Widget myModifyPreview() => Container(); ); await initReloadSub.cancel(); - DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + DTDResponse result = await getPreviews(dtdConnection); FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -232,7 +223,7 @@ Widget myModifyPreview() => Container(); ); await modifyReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map); if (previews.previews.isEmpty) { throw StateError('Preview was lost after modification!'); @@ -295,10 +286,7 @@ Widget myPartPreview() => Container(); ); await reloadSub.cancel(); - final DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + final DTDResponse result = await getPreviews(dtdConnection); final FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index ff475d54d20e4..4b9747f13bf33 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:dtd/dtd.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart'; @@ -150,3 +151,12 @@ Future> runWidgetPreview({ void runFlutterClean(Directory tempDir, [ProcessManager processManager = _processManager]) { processManager.runSync([flutterBin, 'clean'], workingDirectory: tempDir.path); } + +Future getPreviews(DartToolingDaemon dtdConnection) async { + if (platform.isWindows) { + // Give the slow Windows filesystem and analysis server plenty of time + // to finish subsequent analysis runs and rebuild the semantic model. + await Future.delayed(const Duration(seconds: 2)); + } + return dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); +} From 203cfc0d7d91106537d0bf5f13b72b3cf4d622fe Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Thu, 21 May 2026 21:04:47 +0000 Subject: [PATCH 15/15] [Tool] Address review feedback for widget preview - Extract path separator RegExp to a static field in StringExtension to avoid recreation. - Revert the custom delay for waitForAnalysis in LspPreviewDetector to the default to see if it is still needed. --- .../lib/src/widget_preview/lsp_preview_detector.dart | 2 +- packages/flutter_tools/lib/src/widget_preview/utils.dart | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index 60b434e1d8fd1..ab0d7f6e74d12 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -185,7 +185,7 @@ class LspPreviewDetector { onPubspecChangeDetected(filePath); return; } - await _analysisServer?.waitForAnalysis(delay: const Duration(seconds: 5)); + await _analysisServer?.waitForAnalysis(); try { final FlutterWidgetPreviews result = await dtd.getFlutterWidgetPreviews(); onChangeDetected(result); diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index 5e2ffab90bad6..a1374e84cbe71 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -61,10 +61,12 @@ extension AnnotationExtension on Annotation { /// Convenience getters for examining [String] paths. extension StringExtension on String { + static final RegExp _pathSeparator = RegExp(r'[/\\]'); + bool get isDartFile => endsWith('.dart'); bool get isPubspec => endsWith('pubspec.yaml'); bool get doesContainDartTool => contains('.dart_tool'); - bool get doesContainWidgetPreview => split(RegExp(r'[/\\]')).contains('.widget_preview'); + bool get doesContainWidgetPreview => split(_pathSeparator).contains('.widget_preview'); } extension LibraryElementExtension on LibraryElement {