blob: 3b7f48fb93ec5e42dea2bc92eee7ca5e203125e8 [file]
// 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 'dart:async';
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/terminal.dart';
import '../build_info.dart';
import '../bundle.dart' as bundle;
import '../cache.dart';
import '../convert.dart';
import '../dart/analysis.dart';
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';
import '../web/web_device.dart';
import '../widget_preview/analytics.dart';
import '../widget_preview/dependency_graph.dart';
import '../widget_preview/dtd_services.dart';
import '../widget_preview/dtd_types.dart';
import '../widget_preview/lsp_preview_detector.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import '../widget_preview/preview_manifest.dart';
import '../widget_preview/preview_pubspec_builder.dart';
import 'create_base.dart';
class WidgetPreviewCommand extends FlutterCommand {
WidgetPreviewCommand({
required bool verboseHelp,
required Logger logger,
required FileSystem fs,
required FlutterProjectFactory projectFactory,
required Cache cache,
required Platform platform,
required ShutdownHooks shutdownHooks,
required OperatingSystemUtils os,
required ProcessManager processManager,
required Artifacts artifacts,
required Terminal terminal,
@visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride,
@visibleForTesting Future<AnalysisServer> Function()? analysisServerFactoryOverride,
}) {
addSubcommand(
WidgetPreviewStartCommand(
verbose: verboseHelp,
logger: logger,
fs: fs,
projectFactory: projectFactory,
cache: cache,
platform: platform,
shutdownHooks: shutdownHooks,
os: os,
processManager: processManager,
artifacts: artifacts,
dtdServicesOverride: dtdServicesOverride,
analysisServerFactoryOverride: analysisServerFactoryOverride,
terminal: terminal,
),
);
addSubcommand(
WidgetPreviewCleanCommand(logger: logger, fs: fs, projectFactory: projectFactory),
);
}
@override
String get description => 'Manage the widget preview environment.';
@override
String get name => kWidgetPreview;
static const kWidgetPreview = 'widget-preview';
@override
String get category => FlutterCommandCategory.tools;
@override
Future<FlutterCommandResult> runCommand() async => FlutterCommandResult.fail();
}
abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
FileSystem get fs;
Logger get logger;
FlutterProjectFactory get projectFactory;
FlutterProject getRootProject() {
final ArgResults results = argResults!;
final Directory projectDir;
if (results.rest case <String>[final String directory]) {
projectDir = fs.directory(directory);
if (!projectDir.existsSync()) {
throwToolExit('Could not find ${projectDir.path}.');
}
} else if (results.rest.length > 1) {
throwToolExit('Only one directory should be provided.');
} else {
projectDir = fs.currentDirectory;
}
return validateFlutterProjectForPreview(projectDir);
}
FlutterProject validateFlutterProjectForPreview(Directory directory) {
logger.printTrace('Verifying that ${directory.path} is a Flutter project.');
final FlutterProject flutterProject = projectFactory.fromDirectory(directory);
if (!flutterProject.pubspecFile.existsSync()) {
throwToolExit('${flutterProject.directory.path} is not a valid Flutter project.');
}
return flutterProject;
}
}
final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
WidgetPreviewStartCommand({
this.verbose = false,
required Logger logger,
required this.fs,
required this.projectFactory,
required this.cache,
required this.platform,
required this.shutdownHooks,
required this.os,
required this.processManager,
required this.artifacts,
required this.terminal,
@visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride,
@visibleForTesting Future<AnalysisServer> Function()? analysisServerFactoryOverride,
}) : _logger = logger {
if (dtdServicesOverride != null) {
_dtdService = dtdServicesOverride;
}
_analysisServerFactoryOverride = analysisServerFactoryOverride;
addPubOptions();
addMachineOutputFlag(verboseHelp: verbose);
addDevToolsOptions(verboseHelp: verbose);
argParser
..addFlag(
kWebServer,
help:
'Serve the widget preview environment using the web-server device instead of the '
'browser.',
)
..addOption(
kDtdUrl,
help:
'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.',
hide: !verbose,
)
..addFlag(
kLaunchPreviewer,
defaultsTo: true,
help: 'Launches the widget preview environment.',
// Should only be used for testing.
hide: !verbose,
)
..addFlag(kHeadless, help: 'Launches Chrome in headless mode for testing.', hide: !verbose)
..addOption(
kWidgetPreviewScaffoldOutputDir,
help:
'Generated the widget preview environment scaffolding at a given location '
'for testing purposes.',
hide: !verbose,
)
..addFlag(
kDisableDtdServiceUuid,
help: 'Disables the addition of a UUID to the widget preview DTD service and stream.',
hide: !verbose,
)
..addFlag(
kLegacyPreviewDetection,
help:
'Enables the legacy preview detection mechanism that uses '
'package:analyzer instead of LSP.',
hide: !verbose,
);
}
static const kDtdUrl = 'dtd-url';
static const kWidgetPreviewScaffoldName = 'widget_preview_scaffold';
static const kLaunchPreviewer = 'launch-previewer';
static const kHeadless = 'headless';
static const kWebServer = 'web-server';
static const kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
static const kDisableDtdServiceUuid = 'disable-dtd-service-uuid';
static const kLegacyPreviewDetection = 'legacy-preview-detection';
@visibleForTesting
static const kBrowserNotFoundErrorMessage =
'Failed to locate browser. Make sure you are using an up-to-date Chrome or Edge. '
'Otherwise, consider running with --$kWebServer instead.';
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
// Ensure the Flutter Web SDK is installed.
DevelopmentArtifact.web,
};
@override
String get description => 'Starts the widget preview environment.';
@override
String get name => 'start';
final bool verbose;
@override
final FileSystem fs;
@override
WidgetPreviewMachineAwareLogger get logger => _logger as WidgetPreviewMachineAwareLogger;
final Logger _logger;
@override
final FlutterProjectFactory projectFactory;
final Cache cache;
final Platform platform;
final ShutdownHooks shutdownHooks;
final OperatingSystemUtils os;
final ProcessManager processManager;
final Artifacts artifacts;
final Terminal terminal;
late final previewAnalytics = WidgetPreviewAnalytics(analytics: analytics);
late final FlutterProject rootProject = getRootProject();
late final _previewPubspecBuilder = PreviewPubspecBuilder(
logger: logger,
verbose: verbose,
offline: offline,
rootProject: rootProject,
previewManifest: _previewManifest,
);
late final _previewDetector = PreviewDetector(
platform: platform,
previewAnalytics: previewAnalytics,
project: rootProject,
logger: logger,
fs: fs,
onChangeDetected: onLegacyChangeDetected,
onPubspecChangeDetected: _onPubspecChangeDetected,
);
late final _lspPreviewDetector = LspPreviewDetector(
platform: platform,
previewAnalytics: previewAnalytics,
project: rootProject,
logger: logger,
fs: fs,
onChangeDetected: onChangeDetected,
onPubspecChangeDetected: _onPubspecChangeDetected,
shutdownHooks: shutdownHooks,
dtd: _dtdService,
processManager: processManager,
terminal: terminal,
suppressAnalytics: !analytics.okToSend,
analysisServerFactory: _analysisServerFactoryOverride,
artifacts: artifacts,
);
late final Future<AnalysisServer> Function()? _analysisServerFactoryOverride;
late final PreviewCodeGenerator _previewCodeGenerator;
late final _previewManifest = PreviewManifest(
logger: logger,
rootProject: rootProject,
fs: fs,
cache: cache,
);
late var _dtdService = WidgetPreviewDtdServices(
previewAnalytics: previewAnalytics,
fs: fs,
logger: logger,
shutdownHooks: shutdownHooks,
onHotRestartPreviewerRequest: onHotRestartRequest,
dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager),
project: rootProject.widgetPreviewScaffoldProject,
addUuidToServiceName: !boolArg(kDisableDtdServiceUuid),
);
/// The currently running instance of the widget preview scaffold.
ResidentRunner? _widgetPreviewApp;
/// The location of the widget_preview_scaffold for the current execution of the command.
///
/// This is only meant for testing as there's no simple mapping from the target project to the
/// scaffold project.
// TODO(bkonyi): remove once https://github.com/flutter/flutter/issues/179036 is resolved.
@visibleForTesting
static late Directory widgetPreviewScaffold;
@override
Future<FlutterCommandResult> runCommand() async {
assert(_logger is WidgetPreviewMachineAwareLogger);
// Start the timer tracking how long it takes to launch the preview environment.
previewAnalytics.initializeLaunchStopwatch();
logger.sendInitializingEvent();
await WidgetPreviewGitignoreMigration(rootProject, logger).migrate();
final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir);
widgetPreviewScaffold = customPreviewScaffoldOutput != null
? fs.directory(customPreviewScaffoldOutput)
: rootProject.widgetPreviewScaffold;
// Check to see if a preview scaffold has already been generated. If not,
// generate one.
final bool generateScaffoldProject =
customPreviewScaffoldOutput != null || _previewManifest.shouldGenerateProject();
// TODO(bkonyi): can this be moved?
widgetPreviewScaffold.createSync(recursive: true);
fs.currentDirectory = widgetPreviewScaffold;
if (generateScaffoldProject) {
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus(
'Creating widget preview scaffolding at: ${widgetPreviewScaffold.absolute.path}',
);
await generateApp(
<String>['app', kWidgetPreviewScaffoldName],
widgetPreviewScaffold,
createTemplateContext(
organization: 'flutter',
projectName: kWidgetPreviewScaffoldName,
titleCaseProjectName: 'Widget Preview Scaffold',
flutterRoot: Cache.flutterRoot!,
dartSdkVersionBounds: '^${cache.dartSdkBuild}',
web: true,
),
overwrite: true,
generateMetadata: false,
printStatusWhenWriting: verbose,
);
if (customPreviewScaffoldOutput != null) {
return FlutterCommandResult.success();
}
_previewManifest.generate();
// Make the analytics instance aware that we generated the widget preview scaffold as part of
// launching the previewer.
previewAnalytics.generatedProject();
}
// WARNING: this access of widgetPreviewScaffoldProject needs to happen
// 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: widgetPreviewScaffoldProject,
fs: fs,
);
if (generateScaffoldProject || _previewManifest.shouldRegeneratePubspec()) {
if (!generateScaffoldProject) {
logger.printStatus(
'Detected changes in pubspec.yaml. Regenerating pubspec.yaml for the '
'widget preview scaffold.',
);
}
await _previewPubspecBuilder.populatePreviewPubspec(rootProject: rootProject);
}
if (!widgetPreviewScaffoldProject.dartTool.existsSync()) {
await _previewPubspecBuilder.generatePackageConfig(
widgetPreviewScaffoldProject: widgetPreviewScaffoldProject,
);
}
final bool legacyDetection = boolArg('legacy-preview-detection');
shutdownHooks.addShutdownHook(() async {
await _widgetPreviewApp?.exitApp();
if (legacyDetection) {
await _previewDetector.dispose();
} else {
await _lspPreviewDetector.dispose();
}
});
if (legacyDetection) {
final PreviewDependencyGraph graph = await _previewDetector.initialize();
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(graph);
} else {
await configureDtd();
await _lspPreviewDetector.initialize();
_previewCodeGenerator.populateDtdConnectionInfo(
dtdUri: _dtdService.dtdUri!,
widgetPreviewServiceName: _dtdService.widgetPreviewService,
widgetPreviewScaffoldStreamName: _dtdService.widgetPreviewScaffoldStream,
projectRootPath: rootProject.directory.absolute.path,
);
final FlutterWidgetPreviews originalPreviews;
try {
originalPreviews = await _dtdService.getFlutterWidgetPreviews();
} on Exception catch (e) {
throwToolExit(
'Failed to retrieve widget previews from the Dart Tooling Daemon (DTD). '
'Ensure that the analysis server is running and reachable. Details: $e',
);
} on StateError catch (e) {
throwToolExit(
'Failed to retrieve widget previews from the Dart Tooling Daemon (DTD). '
'Ensure that the analysis server is running and reachable. Details: $e',
);
}
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffoldLsp(originalPreviews);
}
final int result = await runPreviewEnvironment(
widgetPreviewScaffoldProject: widgetPreviewScaffoldProject,
);
if (result != 0) {
throwToolExit('Failed to launch the widget previewer.', exitCode: result);
}
return FlutterCommandResult.success();
}
void onLegacyChangeDetected(PreviewDependencyGraph previews) {
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(previews);
logger.printStatus('Triggering reload based on change to preview set: $previews');
_widgetPreviewApp?.restart();
}
void onHotRestartRequest() {
logger.printStatus('Triggering restart based on request from preview environment.');
_widgetPreviewApp?.restart(fullRestart: true);
}
Future<void> _onPubspecChangeDetected(String path) async {
logger.printStatus('Triggering restart based on update to pubspec.yaml: $path');
await _previewPubspecBuilder.populatePreviewPubspec(
rootProject: project,
updatedPubspecPath: path,
);
await _widgetPreviewApp?.restart(fullRestart: true);
}
void onChangeDetected(FlutterWidgetPreviews update) {
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffoldLsp(update);
logger.printStatus('Triggering reload based on update to script: ${update.scriptUris}');
_widgetPreviewApp?.restart();
}
/// Configures the Dart Tooling Daemon connection.
///
/// If --dtd-uri is provided, the existing DTD instance will be used. If the tool fails to
/// connect to this URI, it will start its own DTD instance.
///
/// If --dtd-uri is not provided, a DTD instance managed by the tool will be started.
Future<void> configureDtd() async {
final String? existingDtdUriStr = stringArg(kDtdUrl);
Uri? existingDtdUri;
try {
if (existingDtdUriStr != null) {
existingDtdUri = Uri.parse(existingDtdUriStr);
}
} on FormatException {
logger.printWarning('Failed to parse value of --dtd-uri: $existingDtdUriStr.');
}
if (existingDtdUri != null) {
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
await _dtdService.connect(dtdWsUri: existingDtdUri);
}
}
Future<int> runPreviewEnvironment({required FlutterProject widgetPreviewScaffoldProject}) async {
try {
// In the rare case that Flutter Web is disabled, the device manager will not return any web
// devices which will cause us to crash.
if (!featureFlags.isWebEnabled) {
throwToolExit(
'Widget Previews requires Flutter Web to be enabled. Please run '
"'flutter config --enable-web' to enable Flutter Web and try again.",
);
}
final Device device;
if (boolArg(kWebServer)) {
final List<Device> devices;
try {
// The web-server device is hidden by default, make it visible before trying to look it up.
WebServerDevice.showWebServerDevice = true;
devices = await deviceManager!.getDevicesById(WebServerDevice.kWebServerDeviceId);
} finally {
// Reset the flag to false to avoid affecting other commands.
WebServerDevice.showWebServerDevice = false;
}
assert(devices.length == 1);
device = devices.single;
} else {
// Since the only target supported by the widget preview scaffold is the web
// device, only a single web device should be returned.
final List<Device> devices = await deviceManager!.getDevices(
filter: DeviceDiscoveryFilter(
supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject(
flutterProject: widgetPreviewScaffoldProject,
),
deviceConnectionInterface: DeviceConnectionInterface.attached,
),
);
if (devices.isEmpty) {
throwToolExit(kBrowserNotFoundErrorMessage);
}
if (devices.length > 1) {
// Prefer Google Chrome as the target browser.
device =
devices.firstWhereOrNull((device) => device is GoogleChromeDevice) ?? devices.first;
logger.printTrace(
'Detected ${devices.length} web devices (${devices.map((e) => e.displayName).join(', ')}). '
'Defaulting to ${device.displayName}.',
);
} else {
device = devices.single;
}
}
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Launching the Widget Preview Scaffold on ${device.displayName}...');
final debuggingOptions = DebuggingOptions.enabled(
BuildInfo(
BuildMode.debug,
null,
treeShakeIcons: false,
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
packageConfig: PackageConfig.parseBytes(
widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),
widgetPreviewScaffoldProject.packageConfig.uri,
),
trackWidgetCreation: true,
// Don't try and download canvaskit from the CDN.
useLocalCanvasKit: true,
webEnableHotReload: true,
includeUnsupportedPlatformLibraryStubs: true,
),
webEnableExposeUrl: false,
webEnableExpressionEvaluation: true,
webRunHeadless: boolArg(kHeadless),
devToolsServerAddress: devToolsServerAddress,
);
final String target = bundle.defaultMainPath;
final FlutterDevice flutterDevice = await FlutterDevice.create(
device,
target: target,
buildInfo: debuggingOptions.buildInfo,
platform: platform,
);
if (boolArg(kLaunchPreviewer)) {
final appStarted = Completer<void>();
final connectionInfo = Completer<DebugConnectionInfo>();
_widgetPreviewApp = ResidentWebRunner(
flutterDevice,
target: target,
debuggingOptions: debuggingOptions,
analytics: analytics,
flutterProject: widgetPreviewScaffoldProject,
fileSystem: fs,
logger: logger,
terminal: globals.terminal,
platform: platform,
outputPreferences: globals.outputPreferences,
systemClock: globals.systemClock,
// Explicitly provide the project root path rather than relying on the current directory
// as the current directory exists within $TMP. At least on MacOS, when setting the
// current directory to the widget_preview_scaffold project created under
// `/var/folders/...`, the underlying chdir call actually changes the directory to
// `/private/var/folders/...`. These directories are identical, but confuse the package
// config resolution logic.
// TODO(bkonyi): consider removing if we stop placing the scaffold in $TMP.
// See https://github.com/flutter/flutter/issues/179036
projectRootPath: widgetPreviewScaffoldProject.directory.absolute.path,
);
unawaited(
_widgetPreviewApp!.run(
appStartedCompleter: appStarted,
connectionInfoCompleter: connectionInfo,
),
);
await appStarted.future;
logger.sendStartedEvent(applicationUrl: flutterDevice.devFS!.baseUri!);
final DebugConnectionInfo debugConnection = await connectionInfo.future;
final Uri? devToolsUri = devToolsServerAddress ?? debugConnection.devToolsUri;
if (devToolsUri == null) {
throwToolExit('Could not determine DevTools server address for the widget inspector.');
}
_dtdService.setDevToolsServerAddress(
devToolsServerAddress: devToolsServerAddress ?? debugConnection.devToolsUri!,
applicationUri: debugConnection.wsUri!,
);
}
} on Exception catch (error) {
throwToolExit(error.toString());
}
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Done loading previews.');
// Send an analytics event reporting how long it took for the widget previewer to start.
previewAnalytics.reportLaunchTiming();
// If _widgetPreviewApp is null --no-launch-previewer was provided so return success.
return _widgetPreviewApp?.waitForAppToFinish() ?? 0;
}
}
final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase {
WidgetPreviewCleanCommand({required this.fs, required this.logger, required this.projectFactory});
@override
String get description => 'Cleans up widget preview state.';
@override
String get name => 'clean';
@override
final FileSystem fs;
@override
final Logger logger;
@override
final FlutterProjectFactory projectFactory;
@override
Future<FlutterCommandResult> runCommand() async {
final Directory widgetPreviewScaffold = getRootProject().widgetPreviewScaffold;
if (widgetPreviewScaffold.existsSync()) {
final String scaffoldPath = widgetPreviewScaffold.path;
logger.printStatus('Deleting widget preview scaffold at $scaffoldPath.');
widgetPreviewScaffold.deleteSync(recursive: true);
} else {
logger.printStatus('Nothing to clean up.');
}
return FlutterCommandResult.success();
}
}
/// A custom logger for the widget-preview commands that disables non-event output to stdio when
/// machine mode is enabled.
final class WidgetPreviewMachineAwareLogger extends DelegatingLogger {
WidgetPreviewMachineAwareLogger(super.delegate, {required this.machine, required this.verbose});
final bool machine;
final bool verbose;
@override
void printError(
String message, {
StackTrace? stackTrace,
bool? emphasis,
TerminalColor? color,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{
'level': 'error',
'message': message,
'stackTrace': ?stackTrace?.toString(),
});
return;
}
super.printError(
message,
stackTrace: stackTrace,
emphasis: emphasis,
color: color,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
);
}
@override
void printWarning(
String message, {
bool? emphasis,
TerminalColor? color,
int? indent,
int? hangingIndent,
bool? wrap,
bool fatal = true,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'warning', 'message': message});
return;
}
super.printWarning(
message,
emphasis: emphasis,
color: color,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
fatal: fatal,
);
}
@override
void printStatus(
String message, {
bool? emphasis,
TerminalColor? color,
bool? newline,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'status', 'message': message});
return;
}
super.printStatus(
message,
emphasis: emphasis,
color: color,
newline: newline,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
);
}
@override
void printBox(String message, {String? title}) {
if (machine) {
return;
}
super.printBox(message, title: title);
}
@override
void printTrace(String message) {
if (!verbose) {
return;
}
if (machine) {
sendEvent('logMessage', <String, Object?>{'level': 'trace', 'message': message});
return;
}
super.printTrace(message);
}
/// Notifies tooling that the widget previewer is initializing.
void sendInitializingEvent() {
sendEvent('initializing', {'pid': pid});
}
/// Notifies tooling that the widget previewer has started and is being
/// served at [applicationUrl].
void sendStartedEvent({required Uri applicationUrl}) {
sendEvent('started', {'url': applicationUrl.toString()});
}
@override
void sendEvent(String name, [Map<String, dynamic>? args]) {
if (!machine) {
return;
}
// Don't call super.printStatus as it will result in a prefix being printed when --verbose is
// provided.
globals.stdio.stdout.writeln(
json.encode([
{'event': 'widget_preview.$name', 'params': ?args},
]),
);
}
@override
Status startProgress(
String message, {
String? progressId,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
if (machine) {
printStatus(message);
return SilentStatus(stopwatch: Stopwatch());
}
return super.startProgress(
message,
progressId: progressId,
progressIndicatorPadding: progressIndicatorPadding,
);
}
@override
Status startSpinner({
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
}) {
if (machine) {
return SilentStatus(stopwatch: Stopwatch());
}
return super.startSpinner(
onFinish: onFinish,
timeout: timeout,
slowWarningCallback: slowWarningCallback,
warningColor: warningColor,
);
}
}