[ Widget Preview ] Fix crash when widget_preview_scaffold/.dart_tool doesn't exist (#178662)

If the `.dart_tool/widget_preview_scaffold/.dart_tool/` directory wasn't
created during the initial run of `flutter widget-preview start` due to
the command being interrupted or `pub` being disabled, `flutter
widget-preview start` would crash due to the `package_config.json` logic
walking up the directory structure looking for the nearest
`package_config.json`. This would point to the parent project's
`package_config.json`, which would not be compatible with the widget
preview scaffold project.

Fixes https://github.com/flutter/flutter/issues/178660 and
https://github.com/flutter/flutter/issues/177655
This commit is contained in:
Ben Konyi 2025-11-17 15:27:30 -05:00 committed by GitHub
parent 4cf8eb9bea
commit cc14ef5290
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 51 additions and 8 deletions

View File

@ -318,8 +318,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
// after we generate the scaffold project as invoking the getter triggers
// lazy initialization of the preview scaffold's FlutterManifest before
// the scaffold project's pubspec has been generated.
final FlutterProject widgetPreviewScaffoldProject = rootProject.widgetPreviewScaffoldProject;
_previewCodeGenerator = PreviewCodeGenerator(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
widgetPreviewScaffoldProject: widgetPreviewScaffoldProject,
fs: fs,
);
@ -333,6 +334,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
await _previewPubspecBuilder.populatePreviewPubspec(rootProject: rootProject);
}
if (!widgetPreviewScaffoldProject.dartTool.existsSync()) {
await _previewPubspecBuilder.generatePackageConfig(
widgetPreviewScaffoldProject: widgetPreviewScaffoldProject,
);
}
shutdownHooks.addShutdownHook(() async {
await _widgetPreviewApp?.exitApp();
await _previewDetector.dispose();
@ -343,7 +350,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
await configureDtd();
final int result = await runPreviewEnvironment(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
widgetPreviewScaffoldProject: widgetPreviewScaffoldProject,
);
if (result != 0) {
throwToolExit('Failed to launch the widget previewer.', exitCode: result);

View File

@ -91,6 +91,8 @@ class PreviewPubspecBuilder {
);
}
PubOutputMode get _outputMode => verbose ? PubOutputMode.all : PubOutputMode.failuresOnly;
Future<void> populatePreviewPubspec({
required FlutterProject rootProject,
String? updatedPubspecPath,
@ -126,7 +128,6 @@ class PreviewPubspecBuilder {
}),
};
final PubOutputMode outputMode = verbose ? PubOutputMode.all : PubOutputMode.failuresOnly;
await pub.interactively(
<String>[
pubAdd,
@ -146,7 +147,7 @@ class PreviewPubspecBuilder {
context: PubContext.pubAdd,
command: pubAdd,
touchesPackageConfig: true,
outputMode: outputMode,
outputMode: _outputMode,
);
// Adds dependencies required by the widget preview scaffolding.
@ -161,18 +162,22 @@ class PreviewPubspecBuilder {
context: PubContext.pubAdd,
command: pubAdd,
touchesPackageConfig: true,
outputMode: outputMode,
outputMode: _outputMode,
);
await generatePackageConfig(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject);
previewManifest.updatePubspecHash(updatedPubspecPath: updatedPubspecPath);
}
/// Generates `widget_preview_scaffold/.dart_tool/package_config.json`.
Future<void> generatePackageConfig({required FlutterProject widgetPreviewScaffoldProject}) async {
// Generate package_config.json.
await pub.get(
context: PubContext.create,
project: widgetPreviewScaffoldProject,
offline: offline,
outputMode: outputMode,
outputMode: _outputMode,
);
previewManifest.updatePubspecHash(updatedPubspecPath: updatedPubspecPath);
}
void onPubspecChangeDetected(String path) {

View File

@ -7,6 +7,7 @@ import 'dart:convert';
import 'package:dtd/dtd.dart';
import 'package:file/file.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
@ -125,6 +126,36 @@ void main() {
await runWidgetPreview(expectedMessages: firstLaunchMessagesWebServer, useWebServer: true);
});
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(<String>[
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(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(expectedMessages: firstLaunchMessagesWeb);