[ Widget Preview ] Add support for building and launching the widget preview scaffold (#162326)
This also wires up the preview detector to trigger hot reloads when new previews are detected or previews are removed. Note: while this change results in lib/generated_preview.dart being generated and updated, it's not currently referenced by lib/main.dart and the preview environment will render a black screen. --------- Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
8
.ci.yaml
8
.ci.yaml
@@ -1497,7 +1497,10 @@ targets:
|
||||
dependencies: >-
|
||||
[
|
||||
{"dependency": "android_sdk", "version": "version:35v1"},
|
||||
{"dependency": "open_jdk", "version": "version:17"}
|
||||
{"dependency": "open_jdk", "version": "version:17"},
|
||||
{"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"},
|
||||
{"dependency": "cmake", "version": "build_id:8787856497187628321"},
|
||||
{"dependency": "ninja", "version": "version:1.9.0"}
|
||||
]
|
||||
shard: tool_tests
|
||||
subshard: commands
|
||||
@@ -6322,7 +6325,8 @@ targets:
|
||||
dependencies: >-
|
||||
[
|
||||
{"dependency": "android_sdk", "version": "version:35v1"},
|
||||
{"dependency": "open_jdk", "version": "version:17"}
|
||||
{"dependency": "open_jdk", "version": "version:17"},
|
||||
{"dependency": "vs_build", "version": "version:vs2019"}
|
||||
]
|
||||
shard: tool_tests
|
||||
subshard: commands
|
||||
|
||||
@@ -85,7 +85,10 @@ Future<void> main(List<String> args) async {
|
||||
final bool daemon = args.contains('daemon');
|
||||
final bool runMachine =
|
||||
(args.contains('--machine') && args.contains('run')) ||
|
||||
(args.contains('--machine') && args.contains('attach'));
|
||||
(args.contains('--machine') && args.contains('attach')) ||
|
||||
// `flutter widget-preview start` starts an application that requires a logger
|
||||
// to be setup for machine mode.
|
||||
(args.contains('widget-preview') && args.contains('start'));
|
||||
|
||||
// Cache.flutterRoot must be set early because other features use it (e.g.
|
||||
// enginePath's initializer uses it). This can only work with the real
|
||||
@@ -247,10 +250,14 @@ List<FlutterCommand> generateCommands({required bool verboseHelp, required bool
|
||||
nativeAssetsBuilder: globals.nativeAssetsBuilder,
|
||||
),
|
||||
WidgetPreviewCommand(
|
||||
verboseHelp: verboseHelp,
|
||||
logger: globals.logger,
|
||||
fs: globals.fs,
|
||||
projectFactory: globals.projectFactory,
|
||||
cache: globals.cache,
|
||||
platform: globals.platform,
|
||||
shutdownHooks: globals.shutdownHooks,
|
||||
os: globals.os,
|
||||
),
|
||||
UpgradeCommand(verboseHelp: verboseHelp),
|
||||
SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),
|
||||
|
||||
@@ -195,6 +195,21 @@ class Daemon {
|
||||
);
|
||||
}
|
||||
|
||||
factory Daemon.createMachineDaemon() {
|
||||
final Daemon daemon = Daemon(
|
||||
DaemonConnection(
|
||||
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
|
||||
logger: globals.logger,
|
||||
),
|
||||
notifyingLogger:
|
||||
(globals.logger is NotifyingLogger)
|
||||
? globals.logger as NotifyingLogger
|
||||
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
|
||||
logToStdout: true,
|
||||
);
|
||||
return daemon;
|
||||
}
|
||||
|
||||
final DaemonConnection connection;
|
||||
|
||||
late DaemonDomain daemonDomain;
|
||||
@@ -809,15 +824,11 @@ class AppDomain extends Domain {
|
||||
);
|
||||
}
|
||||
final Completer<void> appStartedCompleter = Completer<void>();
|
||||
// We don't want to wait for this future to complete, and callbacks won't fail,
|
||||
// as it just writes to stdout.
|
||||
unawaited(
|
||||
appStartedCompleter.future.then<void>((void value) {
|
||||
_sendAppEvent(app, 'started');
|
||||
}),
|
||||
);
|
||||
|
||||
await app._runInZone<void>(this, () async {
|
||||
// This future won't complete until the application has shutdown, so we don't want to
|
||||
// await it. However, we do need to listen to the future in order to handle possible
|
||||
// tool exits
|
||||
final Future<void> appRunFuture = app._runInZone<void>(this, () async {
|
||||
try {
|
||||
await runOrAttach(
|
||||
connectionInfoCompleter: connectionInfoCompleter,
|
||||
@@ -836,6 +847,13 @@ class AppDomain extends Domain {
|
||||
_apps.remove(app);
|
||||
}
|
||||
});
|
||||
|
||||
await Future.any(<Future<void>>[
|
||||
appStartedCompleter.future.then<void>((void value) {
|
||||
_sendAppEvent(app, 'started');
|
||||
}),
|
||||
appRunFuture,
|
||||
]);
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/utils.dart';
|
||||
import '../build_info.dart';
|
||||
import '../daemon.dart';
|
||||
import '../device.dart';
|
||||
import '../features.dart';
|
||||
import '../globals.dart' as globals;
|
||||
@@ -761,18 +760,7 @@ class RunCommand extends RunCommandBase {
|
||||
|
||||
@visibleForTesting
|
||||
Daemon createMachineDaemon() {
|
||||
final Daemon daemon = Daemon(
|
||||
DaemonConnection(
|
||||
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
|
||||
logger: globals.logger,
|
||||
),
|
||||
notifyingLogger:
|
||||
(globals.logger is NotifyingLogger)
|
||||
? globals.logger as NotifyingLogger
|
||||
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
|
||||
logToStdout: true,
|
||||
);
|
||||
return daemon;
|
||||
return Daemon.createMachineDaemon();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,31 +9,47 @@ import '../base/common.dart';
|
||||
import '../base/deferred_component.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_info.dart';
|
||||
import '../bundle.dart' as bundle;
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../dart/pub.dart';
|
||||
import '../device.dart';
|
||||
import '../flutter_manifest.dart';
|
||||
|
||||
import '../linux/build_linux.dart';
|
||||
import '../macos/build_macos.dart';
|
||||
import '../project.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
import '../widget_preview/preview_code_generator.dart';
|
||||
import '../widget_preview/preview_detector.dart';
|
||||
import '../windows/build_windows.dart';
|
||||
import 'create_base.dart';
|
||||
import 'daemon.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,
|
||||
}) {
|
||||
addSubcommand(
|
||||
WidgetPreviewStartCommand(
|
||||
verboseHelp: verboseHelp,
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
projectFactory: projectFactory,
|
||||
cache: cache,
|
||||
platform: platform,
|
||||
shutdownHooks: shutdownHooks,
|
||||
os: os,
|
||||
),
|
||||
);
|
||||
addSubcommand(
|
||||
@@ -92,20 +108,36 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
|
||||
|
||||
final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
|
||||
WidgetPreviewStartCommand({
|
||||
this.verboseHelp = false,
|
||||
required this.logger,
|
||||
required this.fs,
|
||||
required this.projectFactory,
|
||||
required this.cache,
|
||||
required this.platform,
|
||||
required this.shutdownHooks,
|
||||
required this.os,
|
||||
}) {
|
||||
addPubOptions();
|
||||
argParser.addFlag(
|
||||
kLaunchPreviewer,
|
||||
defaultsTo: true,
|
||||
help: 'Launches the widget preview environment.',
|
||||
// Should only be used for testing.
|
||||
hide: !verboseHelp,
|
||||
);
|
||||
}
|
||||
|
||||
static const String kWidgetPreviewScaffoldName = 'widget_preview_scaffold';
|
||||
static const String kLaunchPreviewer = 'launch-previewer';
|
||||
|
||||
@override
|
||||
String get description => 'Starts the widget preview environment.';
|
||||
|
||||
@override
|
||||
String get name => 'start';
|
||||
|
||||
final bool verboseHelp;
|
||||
|
||||
@override
|
||||
final FileSystem fs;
|
||||
|
||||
@@ -117,6 +149,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
final Cache cache;
|
||||
|
||||
final Platform platform;
|
||||
|
||||
final ShutdownHooks shutdownHooks;
|
||||
|
||||
final OperatingSystemUtils os;
|
||||
|
||||
late final PreviewDetector _previewDetector = PreviewDetector(
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
@@ -125,6 +163,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
late final PreviewCodeGenerator _previewCodeGenerator;
|
||||
|
||||
/// The currently running instance of the widget preview scaffold.
|
||||
AppInstance? _widgetPreviewApp;
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
final FlutterProject rootProject = getRootProject();
|
||||
@@ -132,44 +173,231 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
// Check to see if a preview scaffold has already been generated. If not,
|
||||
// generate one.
|
||||
if (!widgetPreviewScaffold.existsSync()) {
|
||||
final bool generateScaffoldProject = !widgetPreviewScaffold.existsSync();
|
||||
widgetPreviewScaffold.createSync();
|
||||
|
||||
if (generateScaffoldProject) {
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Creating widget preview scaffolding at: ${widgetPreviewScaffold.path}');
|
||||
await generateApp(
|
||||
<String>['widget_preview_scaffold'],
|
||||
<String>['app', kWidgetPreviewScaffoldName],
|
||||
widgetPreviewScaffold,
|
||||
createTemplateContext(
|
||||
organization: 'flutter',
|
||||
projectName: 'widget_preview_scaffold',
|
||||
projectName: kWidgetPreviewScaffoldName,
|
||||
titleCaseProjectName: 'Widget Preview Scaffold',
|
||||
flutterRoot: Cache.flutterRoot!,
|
||||
dartSdkVersionBounds: '^${cache.dartSdkBuild}',
|
||||
linux: const LocalPlatform().isLinux,
|
||||
macos: const LocalPlatform().isMacOS,
|
||||
windows: const LocalPlatform().isWindows,
|
||||
linux: platform.isLinux,
|
||||
macos: platform.isMacOS,
|
||||
windows: platform.isWindows,
|
||||
),
|
||||
overwrite: true,
|
||||
generateMetadata: false,
|
||||
);
|
||||
await _populatePreviewPubspec(rootProject: rootProject);
|
||||
|
||||
// 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.
|
||||
// TODO(bkonyi): add logic to rebuild after SDK updates
|
||||
await initialBuild(widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject);
|
||||
}
|
||||
|
||||
// WARNING: this needs to happen after we generate the scaffold project as invoking the
|
||||
// widgetPreviewScaffoldProject getter triggers lazy initialization of the preview scaffold's
|
||||
// FlutterManifest before the scaffold project's pubspec has been generated.
|
||||
_previewCodeGenerator = PreviewCodeGenerator(
|
||||
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
|
||||
fs: fs,
|
||||
);
|
||||
|
||||
// TODO(matanlurey): Remove this comment once flutter_gen is removed.
|
||||
//
|
||||
// Tracking removal: https://github.com/flutter/flutter/issues/102983.
|
||||
//
|
||||
// Populate the pubspec after the initial build to avoid blowing away the package_config.json
|
||||
// which may have manual changes for flutter_gen support.
|
||||
await _populatePreviewPubspec(rootProject: rootProject);
|
||||
|
||||
final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory);
|
||||
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
|
||||
|
||||
if (boolArg(kLaunchPreviewer)) {
|
||||
shutdownHooks.addShutdownHook(() async {
|
||||
await _widgetPreviewApp?.stop();
|
||||
});
|
||||
_widgetPreviewApp = await runPreviewEnvironment(
|
||||
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
|
||||
);
|
||||
final int result = await _widgetPreviewApp!.runner.waitForAppToFinish();
|
||||
if (result != 0) {
|
||||
throwToolExit('Failed to launch the widget previewer.', exitCode: result);
|
||||
}
|
||||
}
|
||||
|
||||
await _previewDetector.dispose();
|
||||
return FlutterCommandResult.success();
|
||||
}
|
||||
|
||||
void onChangeDetected(PreviewMapping previews) {
|
||||
// TODO(bkonyi): perform hot reload
|
||||
logger.printStatus('Triggering reload based on change to preview set: $previews');
|
||||
_widgetPreviewApp?.restart();
|
||||
}
|
||||
|
||||
/// Builds the application binary for the widget preview scaffold the first
|
||||
/// time the widget preview command is run.
|
||||
///
|
||||
/// The resulting binary is used to speed up subsequent widget previewer launches
|
||||
/// by acting as a basic scaffold to load previews into using hot reload / restart.
|
||||
Future<void> initialBuild({required FlutterProject widgetPreviewScaffoldProject}) async {
|
||||
// TODO(bkonyi): handle error case where desktop device isn't enabled.
|
||||
await widgetPreviewScaffoldProject.ensureReadyForPlatformSpecificTooling(
|
||||
linuxPlatform: platform.isLinux,
|
||||
macOSPlatform: platform.isMacOS,
|
||||
windowsPlatform: platform.isWindows,
|
||||
allowedPlugins: const <String>[],
|
||||
);
|
||||
|
||||
// Generate initial package_config.json, otherwise the build will fail.
|
||||
await pub.get(
|
||||
context: PubContext.create,
|
||||
project: widgetPreviewScaffoldProject,
|
||||
offline: offline,
|
||||
outputMode: PubOutputMode.summaryOnly,
|
||||
);
|
||||
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Performing initial build of the Widget Preview Scaffold...');
|
||||
|
||||
final BuildInfo buildInfo = BuildInfo(
|
||||
BuildMode.debug,
|
||||
null,
|
||||
treeShakeIcons: false,
|
||||
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
|
||||
);
|
||||
|
||||
if (platform.isMacOS) {
|
||||
await buildMacOS(
|
||||
flutterProject: widgetPreviewScaffoldProject,
|
||||
buildInfo: buildInfo,
|
||||
verboseLogging: false,
|
||||
);
|
||||
} else if (platform.isLinux) {
|
||||
await buildLinux(
|
||||
widgetPreviewScaffoldProject.linux,
|
||||
buildInfo,
|
||||
targetPlatform:
|
||||
os.hostPlatform == HostPlatform.linux_x64
|
||||
? TargetPlatform.linux_x64
|
||||
: TargetPlatform.linux_arm64,
|
||||
logger: logger,
|
||||
);
|
||||
} else if (platform.isWindows) {
|
||||
await buildWindows(
|
||||
widgetPreviewScaffoldProject.windows,
|
||||
buildInfo,
|
||||
os.hostPlatform == HostPlatform.windows_x64
|
||||
? TargetPlatform.windows_x64
|
||||
: TargetPlatform.windows_arm64,
|
||||
);
|
||||
} else {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Widget Preview Scaffold initial build complete.');
|
||||
}
|
||||
|
||||
/// Returns the path to a prebuilt widget_preview_scaffold application binary.
|
||||
String prebuiltApplicationBinaryPath({required FlutterProject widgetPreviewScaffoldProject}) {
|
||||
assert(platform.isLinux || platform.isMacOS || platform.isWindows);
|
||||
String path;
|
||||
if (platform.isMacOS) {
|
||||
path = fs.path.join(
|
||||
getMacOSBuildDirectory(),
|
||||
'Build/Products/Debug/widget_preview_scaffold.app',
|
||||
);
|
||||
} else if (platform.isLinux) {
|
||||
path = fs.path.join(
|
||||
getLinuxBuildDirectory(
|
||||
os.hostPlatform == HostPlatform.linux_x64
|
||||
? TargetPlatform.linux_x64
|
||||
: TargetPlatform.linux_arm64,
|
||||
),
|
||||
'debug/bundle/widget_preview_scaffold',
|
||||
);
|
||||
} else if (platform.isWindows) {
|
||||
path = fs.path.join(
|
||||
getWindowsBuildDirectory(
|
||||
os.hostPlatform == HostPlatform.windows_x64
|
||||
? TargetPlatform.windows_x64
|
||||
: TargetPlatform.windows_arm64,
|
||||
),
|
||||
'runner/Debug/widget_preview_scaffold.exe',
|
||||
);
|
||||
} else {
|
||||
throw StateError('Unknown OS');
|
||||
}
|
||||
path = fs.path.join(widgetPreviewScaffoldProject.directory.path, path);
|
||||
if (fs.typeSync(path) == FileSystemEntityType.notFound) {
|
||||
logger.printStatus(fs.currentDirectory.toString());
|
||||
throw StateError('Could not find prebuilt application binary at $path.');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<AppInstance> runPreviewEnvironment({
|
||||
required FlutterProject widgetPreviewScaffoldProject,
|
||||
}) async {
|
||||
final AppInstance app;
|
||||
try {
|
||||
// Since the only target supported by the widget preview scaffold is the host's desktop
|
||||
// device, only a single desktop device should be returned.
|
||||
final List<Device> devices = await deviceManager!.getDevices(
|
||||
filter: DeviceDiscoveryFilter(
|
||||
supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject(
|
||||
flutterProject: widgetPreviewScaffoldProject,
|
||||
),
|
||||
deviceConnectionInterface: DeviceConnectionInterface.attached,
|
||||
),
|
||||
);
|
||||
assert(devices.length == 1);
|
||||
final Device device = devices.first;
|
||||
|
||||
// We launch from a prebuilt widget preview scaffold instance to reduce launch times after
|
||||
// the first run.
|
||||
final File prebuiltApplicationBinary = fs.file(
|
||||
prebuiltApplicationBinaryPath(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject),
|
||||
);
|
||||
const String? kEmptyRoute = null;
|
||||
const bool kEnableHotReload = true;
|
||||
|
||||
app = await Daemon.createMachineDaemon().appDomain.startApp(
|
||||
device,
|
||||
widgetPreviewScaffoldProject.directory.path,
|
||||
bundle.defaultMainPath,
|
||||
kEmptyRoute, // route
|
||||
DebuggingOptions.enabled(
|
||||
BuildInfo(
|
||||
BuildMode.debug,
|
||||
null,
|
||||
treeShakeIcons: false,
|
||||
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
|
||||
),
|
||||
),
|
||||
kEnableHotReload, // hot mode
|
||||
applicationBinary: prebuiltApplicationBinary,
|
||||
trackWidgetCreation: false,
|
||||
projectRootPath: widgetPreviewScaffoldProject.directory.path,
|
||||
);
|
||||
} on Exception catch (error) {
|
||||
throwToolExit(error.toString());
|
||||
}
|
||||
// Immediately perform a hot restart to ensure new previews are loaded into the prebuilt
|
||||
// application.
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Loading previews into the Widget Preview Scaffold...');
|
||||
await app.restart(fullRestart: true);
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Done loading previews.');
|
||||
return app;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
@@ -80,9 +80,9 @@ Future<void> buildLinux(
|
||||
|
||||
final Status status = logger.startProgress('Building Linux application...');
|
||||
final String buildModeName = buildInfo.mode.cliName;
|
||||
final Directory platformBuildDirectory = globals.fs.directory(
|
||||
getLinuxBuildDirectory(targetPlatform),
|
||||
);
|
||||
final Directory platformBuildDirectory = globals.fs
|
||||
.directory(linuxProject.parent.directory.path)
|
||||
.childDirectory(getLinuxBuildDirectory(targetPlatform));
|
||||
final Directory buildDirectory = platformBuildDirectory.childDirectory(buildModeName);
|
||||
try {
|
||||
await _runCmake(
|
||||
|
||||
@@ -178,6 +178,7 @@ class BuildableMacOSApp extends MacOSApp {
|
||||
}
|
||||
|
||||
return globals.fs.path.join(
|
||||
project.parent.directory.path,
|
||||
getMacOSBuildDirectory(),
|
||||
'Build',
|
||||
'Products',
|
||||
|
||||
@@ -111,7 +111,9 @@ Future<void> buildMacOS({
|
||||
final ProjectMigration migration = ProjectMigration(migrators);
|
||||
await migration.run();
|
||||
|
||||
final Directory flutterBuildDir = globals.fs.directory(getMacOSBuildDirectory());
|
||||
final Directory flutterBuildDir = flutterProject.directory.childDirectory(
|
||||
getMacOSBuildDirectory(),
|
||||
);
|
||||
if (!flutterBuildDir.existsSync()) {
|
||||
flutterBuildDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
@@ -52,13 +52,13 @@ class PreviewCodeGenerator {
|
||||
b
|
||||
..body =
|
||||
literalList(<Object?>[
|
||||
for (final MapEntry<String, List<String>>(
|
||||
key: String path,
|
||||
for (final MapEntry<PreviewPath, List<String>>(
|
||||
key: (path: String _, :Uri uri),
|
||||
value: List<String> previewMethods,
|
||||
)
|
||||
in previews.entries) ...<Object?>[
|
||||
for (final String method in previewMethods)
|
||||
refer(method, path).call(<Expression>[]),
|
||||
refer(method, uri.toString()).call(<Expression>[]),
|
||||
],
|
||||
]).code
|
||||
..name = 'previews'
|
||||
|
||||
@@ -18,7 +18,15 @@ import '../base/logger.dart';
|
||||
import '../base/utils.dart';
|
||||
import 'preview_code_generator.dart';
|
||||
|
||||
typedef PreviewMapping = Map<String, List<String>>;
|
||||
/// A path / URI pair used to map previews to a file.
|
||||
///
|
||||
/// We don't just use a path or a URI as the file watcher doesn't report URIs
|
||||
/// (e.g., package:*) but the analyzer APIs do, and the code generator emits
|
||||
/// package URIs for preview imports.
|
||||
typedef PreviewPath = ({String path, Uri uri});
|
||||
|
||||
/// Represents a set of previews for a given file.
|
||||
typedef PreviewMapping = Map<PreviewPath, List<String>>;
|
||||
|
||||
class PreviewDetector {
|
||||
PreviewDetector({required this.fs, required this.logger, required this.onChangeDetected});
|
||||
@@ -38,7 +46,7 @@ class PreviewDetector {
|
||||
final Watcher watcher = Watcher(projectRoot.path);
|
||||
// TODO(bkonyi): watch for changes to pubspec.yaml
|
||||
_fileWatcher = watcher.events.listen((WatchEvent event) async {
|
||||
final String eventPath = Uri.file(event.path).toString();
|
||||
final String eventPath = event.path;
|
||||
// Only trigger a reload when changes to Dart sources are detected. We
|
||||
// ignore the generated preview file to avoid getting stuck in a loop.
|
||||
if (!eventPath.endsWith('.dart') ||
|
||||
@@ -49,7 +57,9 @@ class PreviewDetector {
|
||||
final PreviewMapping filePreviewsMapping = findPreviewFunctions(
|
||||
fs.file(Uri.file(event.path)),
|
||||
);
|
||||
if (filePreviewsMapping.isEmpty && !_pathToPreviews.containsKey(eventPath)) {
|
||||
final bool hasExistingPreviews =
|
||||
_pathToPreviews.keys.where((PreviewPath e) => e.path == event.path).isNotEmpty;
|
||||
if (filePreviewsMapping.isEmpty && !hasExistingPreviews) {
|
||||
// No previews found or removed, nothing to do.
|
||||
return;
|
||||
}
|
||||
@@ -59,20 +69,21 @@ class PreviewDetector {
|
||||
}
|
||||
if (filePreviewsMapping.isNotEmpty) {
|
||||
// The set of previews has changed, but there are still previews in the file.
|
||||
final MapEntry<String, List<String>>(key: String uri, value: List<String> filePreviews) =
|
||||
filePreviewsMapping.entries.first;
|
||||
assert(uri == eventPath);
|
||||
logger.printStatus('Updated previews for $eventPath: $filePreviews');
|
||||
final MapEntry<PreviewPath, List<String>>(
|
||||
key: PreviewPath location,
|
||||
value: List<String> filePreviews,
|
||||
) = filePreviewsMapping.entries.first;
|
||||
logger.printStatus('Updated previews for ${location.uri}: $filePreviews');
|
||||
if (filePreviews.isNotEmpty) {
|
||||
final List<String>? currentPreviewsForFile = _pathToPreviews[eventPath];
|
||||
final List<String>? currentPreviewsForFile = _pathToPreviews[location];
|
||||
if (filePreviews != currentPreviewsForFile) {
|
||||
_pathToPreviews[eventPath] = filePreviews;
|
||||
_pathToPreviews[location] = filePreviews;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The file previously had previews that were removed.
|
||||
logger.printStatus('Previews removed from $eventPath');
|
||||
_pathToPreviews.remove(eventPath);
|
||||
_pathToPreviews.removeWhere((PreviewPath e, _) => e.path == eventPath);
|
||||
}
|
||||
onChangeDetected(_pathToPreviews);
|
||||
});
|
||||
@@ -106,7 +117,8 @@ class PreviewDetector {
|
||||
final SomeParsedLibraryResult lib = context.currentSession.getParsedLibrary(filePath);
|
||||
if (lib is ParsedLibraryResult) {
|
||||
for (final ParsedUnitResult unit in lib.units) {
|
||||
final List<String> previewEntries = previews[unit.uri.toString()] ?? <String>[];
|
||||
final List<String> previewEntries =
|
||||
previews[(path: unit.path, uri: unit.uri)] ?? <String>[];
|
||||
for (final SyntacticEntity entity in unit.unit.childEntities) {
|
||||
if (entity is FunctionDeclaration && !entity.name.toString().startsWith('_')) {
|
||||
bool foundPreview = false;
|
||||
@@ -127,7 +139,7 @@ class PreviewDetector {
|
||||
}
|
||||
}
|
||||
if (previewEntries.isNotEmpty) {
|
||||
previews[unit.uri.toString()] = previewEntries;
|
||||
previews[(path: unit.path, uri: unit.uri)] = previewEntries;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
Future<void> main() async {
|
||||
// TODO(bkonyi): implement.
|
||||
// TODO(bkonyi): Implement.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Text('Hello World!'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ void main() {
|
||||
'-DFLUTTER_TARGET_PLATFORM=linux-$target',
|
||||
'/linux',
|
||||
],
|
||||
workingDirectory: 'build/linux/$target/$buildMode',
|
||||
workingDirectory: '/build/linux/$target/$buildMode',
|
||||
onRun: onRun,
|
||||
);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ void main() {
|
||||
String stdout = '',
|
||||
}) {
|
||||
return FakeCommand(
|
||||
command: <String>['ninja', '-C', 'build/linux/$target/$buildMode', 'install'],
|
||||
command: <String>['ninja', '-C', '/build/linux/$target/$buildMode', 'install'],
|
||||
environment: environment,
|
||||
onRun: onRun,
|
||||
stdout: stdout,
|
||||
@@ -396,7 +396,7 @@ void main() {
|
||||
// This contains a mix of routine build output and various types of errors
|
||||
// (Dart error, compile error, link error), edited down for compactness.
|
||||
const String stdout = r'''
|
||||
ninja: Entering directory `build/linux/x64/release'
|
||||
ninja: Entering directory `/build/linux/x64/release'
|
||||
[1/6] Generating /foo/linux/flutter/ephemeral/libflutter_linux_gtk.so, /foo/linux/flutter/ephemeral/flutter_linux/flutter_linux.h, _phony
|
||||
lib/main.dart:4:3: Error: Method not found: 'foo'.
|
||||
[2/6] Building CXX object CMakeFiles/foo.dir/main.cc.o
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/os.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/process.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
||||
import 'package:flutter_tools/src/convert.dart';
|
||||
@@ -24,6 +27,7 @@ void main() {
|
||||
late WidgetPreviewStartCommand command;
|
||||
late FlutterProject rootProject;
|
||||
late Logger logger;
|
||||
final Platform platform = FakePlatform.fromPlatform(const LocalPlatform());
|
||||
|
||||
setUp(() {
|
||||
fileSystem = MemoryFileSystem.test();
|
||||
@@ -33,7 +37,15 @@ void main() {
|
||||
fs: fileSystem,
|
||||
projectFactory: FakeFlutterProjectFactory(),
|
||||
logger: logger,
|
||||
cache: Cache.test(processManager: processManager),
|
||||
cache: Cache.test(processManager: processManager, platform: platform),
|
||||
platform: platform,
|
||||
shutdownHooks: ShutdownHooks(),
|
||||
os: OperatingSystemUtils(
|
||||
fileSystem: fileSystem,
|
||||
logger: logger,
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
),
|
||||
);
|
||||
rootProject = FakeFlutterProject(
|
||||
projectRoot: 'some_project',
|
||||
|
||||
@@ -10,7 +10,9 @@ import 'package:flutter_tools/src/base/bot_detector.dart';
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/os.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/process.dart';
|
||||
import 'package:flutter_tools/src/base/signals.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
||||
@@ -40,7 +42,7 @@ void main() {
|
||||
botDetector = const FakeBotDetector(false);
|
||||
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
|
||||
mockStdio = FakeStdio();
|
||||
platform = const LocalPlatform();
|
||||
platform = FakePlatform.fromPlatform(const LocalPlatform());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -58,10 +60,19 @@ void main() {
|
||||
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
|
||||
final CommandRunner<void> runner = createTestCommandRunner(
|
||||
WidgetPreviewCommand(
|
||||
verboseHelp: false,
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
projectFactory: FlutterProjectFactory(logger: logger, fileSystem: fs),
|
||||
cache: Cache.test(processManager: loggingProcessManager, platform: platform),
|
||||
platform: platform,
|
||||
shutdownHooks: ShutdownHooks(),
|
||||
os: OperatingSystemUtils(
|
||||
fileSystem: fs,
|
||||
processManager: loggingProcessManager,
|
||||
logger: logger,
|
||||
platform: platform,
|
||||
),
|
||||
),
|
||||
);
|
||||
await runner.run(<String>['widget-preview', ...arguments]);
|
||||
@@ -74,6 +85,7 @@ void main() {
|
||||
await runWidgetPreviewCommand(<String>[
|
||||
'start',
|
||||
...?arguments,
|
||||
'--no-launch-previewer',
|
||||
if (rootProject != null) rootProject.path,
|
||||
]);
|
||||
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
|
||||
@@ -245,7 +257,7 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'clean deletes .dart_tool/widget_preview_scaffold',
|
||||
'start finds existing previews in the CWD and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
|
||||
() async {
|
||||
final Directory rootProject = await createRootProject();
|
||||
await startWidgetPreview(rootProject: rootProject);
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/flutter_manifest.dart';
|
||||
import 'package:flutter_tools/src/project.dart';
|
||||
import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart';
|
||||
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../src/context.dart';
|
||||
@@ -39,9 +40,9 @@ void main() {
|
||||
expect(generatedPreviewFile, isNot(exists));
|
||||
|
||||
// Populate the generated preview file.
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{
|
||||
'foo.dart': <String>['preview'],
|
||||
'src/bar.dart': <String>['barPreview1', 'barPreview2'],
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(<PreviewPath, List<String>>{
|
||||
(path: '', uri: Uri(path: 'foo.dart')): <String>['preview'],
|
||||
(path: '', uri: Uri(path: 'src/bar.dart')): <String>['barPreview1', 'barPreview2'],
|
||||
});
|
||||
expect(generatedPreviewFile, exists);
|
||||
|
||||
@@ -59,7 +60,9 @@ import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:widget_pre
|
||||
expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents);
|
||||
|
||||
// Regenerate the generated file with no previews.
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{});
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(
|
||||
const <PreviewPath, List<String>>{},
|
||||
);
|
||||
expect(generatedPreviewFile, exists);
|
||||
|
||||
// The generated file should only contain:
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/signals.dart';
|
||||
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
|
||||
import 'package:test/test.dart';
|
||||
@@ -17,16 +18,20 @@ Directory createBasicProjectStructure(FileSystem fs) {
|
||||
return fs.systemTempDirectory.createTempSync('root');
|
||||
}
|
||||
|
||||
File addPreviewContainingFile(Directory projectRoot, String path) {
|
||||
return projectRoot.childDirectory('lib').childFile(path)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(previewContainingFileContents);
|
||||
PreviewPath addPreviewContainingFile(Directory projectRoot, List<String> path) {
|
||||
final File file =
|
||||
projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(previewContainingFileContents);
|
||||
return (path: file.path, uri: file.uri);
|
||||
}
|
||||
|
||||
File addNonPreviewContainingFile(Directory projectRoot, String path) {
|
||||
return projectRoot.childDirectory('lib').childFile(path)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(nonPreviewContainingFileContents);
|
||||
PreviewPath addNonPreviewContainingFile(Directory projectRoot, List<String> path) {
|
||||
final File file =
|
||||
projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(nonPreviewContainingFileContents);
|
||||
return (path: file.path, uri: file.uri);
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -62,29 +67,32 @@ void main() {
|
||||
});
|
||||
|
||||
testUsingContext('can detect previews in existing files', () async {
|
||||
final List<File> previewFiles = <File>[
|
||||
addPreviewContainingFile(projectRoot, 'foo.dart'),
|
||||
addPreviewContainingFile(projectRoot, 'src/bar.dart'),
|
||||
final List<PreviewPath> previewFiles = <PreviewPath>[
|
||||
addPreviewContainingFile(projectRoot, <String>['foo.dart']),
|
||||
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']),
|
||||
];
|
||||
addNonPreviewContainingFile(projectRoot, 'baz.dart');
|
||||
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot);
|
||||
expect(mapping.keys.toSet(), previewFiles.map((File e) => e.uri.toString()).toSet());
|
||||
expect(mapping.keys.toSet(), previewFiles.toSet());
|
||||
});
|
||||
|
||||
testUsingContext('can detect previews in updated files', () async {
|
||||
// Create two files with existing previews and one without.
|
||||
final PreviewMapping expectedInitialMapping = <String, List<String>>{
|
||||
addPreviewContainingFile(projectRoot, 'foo.dart').uri.toString(): <String>['previews'],
|
||||
addPreviewContainingFile(projectRoot, 'src/bar.dart').uri.toString(): <String>['previews'],
|
||||
final PreviewMapping expectedInitialMapping = <PreviewPath, List<String>>{
|
||||
addPreviewContainingFile(projectRoot, <String>['foo.dart']): <String>['previews'],
|
||||
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']): <String>['previews'],
|
||||
};
|
||||
final File nonPreviewContainingFile = addNonPreviewContainingFile(projectRoot, 'baz.dart');
|
||||
final PreviewPath nonPreviewContainingFile = addNonPreviewContainingFile(
|
||||
projectRoot,
|
||||
<String>['baz.dart'],
|
||||
);
|
||||
|
||||
Completer<void> completer = Completer<void>();
|
||||
onChangeDetected = (PreviewMapping updated) {
|
||||
// The new preview in baz.dart should be included in the preview mapping.
|
||||
expect(updated, <String, List<String>>{
|
||||
expect(updated, <PreviewPath, List<String>>{
|
||||
...expectedInitialMapping,
|
||||
nonPreviewContainingFile.uri.toString(): <String>['previews'],
|
||||
nonPreviewContainingFile: <String>['previews'],
|
||||
});
|
||||
completer.complete();
|
||||
};
|
||||
@@ -94,7 +102,7 @@ void main() {
|
||||
|
||||
// Update the file without an existing preview to include a preview and ensure it triggers
|
||||
// the preview detector.
|
||||
addPreviewContainingFile(projectRoot, 'baz.dart');
|
||||
addPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
await completer.future;
|
||||
|
||||
completer = Completer<void>();
|
||||
@@ -106,7 +114,7 @@ void main() {
|
||||
|
||||
// Update the file with an existing preview to remove the preview and ensure it triggers
|
||||
// the preview detector.
|
||||
addNonPreviewContainingFile(projectRoot, 'baz.dart');
|
||||
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
await completer.future;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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 'dart:convert';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/basic_project.dart';
|
||||
import 'test_utils.dart';
|
||||
|
||||
const List<String> firstLaunchMessages = <String>[
|
||||
'Creating widget preview scaffolding at:',
|
||||
'Performing initial build of the Widget Preview Scaffold...',
|
||||
'Widget Preview Scaffold initial build complete.',
|
||||
'Loading previews into the Widget Preview Scaffold...',
|
||||
'Done loading previews.',
|
||||
];
|
||||
|
||||
const List<String> subsequentLaunchMessages = <String>[
|
||||
'Loading previews into the Widget Preview Scaffold...',
|
||||
'Done loading previews.',
|
||||
];
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
Process? process;
|
||||
final BasicProject project = BasicProject();
|
||||
const ProcessManager processManager = LocalProcessManager();
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
|
||||
await project.setUpIn(tempDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
process?.kill();
|
||||
process = null;
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
Future<void> runWidgetPreview({required List<String> expectedMessages}) async {
|
||||
expect(expectedMessages, isNotEmpty);
|
||||
int i = 0;
|
||||
process = await processManager.start(<String>[
|
||||
flutterBin,
|
||||
'widget-preview',
|
||||
'start',
|
||||
], workingDirectory: tempDir.path);
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
|
||||
printOnFailure('STDOUT: $msg');
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (msg.contains(expectedMessages[i])) {
|
||||
++i;
|
||||
}
|
||||
if (i == expectedMessages.length) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
|
||||
printOnFailure('STDERR: $msg');
|
||||
});
|
||||
|
||||
await completer.future;
|
||||
process!.kill();
|
||||
process = null;
|
||||
}
|
||||
|
||||
group('flutter widget-preview start', () {
|
||||
testWithoutContext('smoke test', () async {
|
||||
await runWidgetPreview(expectedMessages: firstLaunchMessages);
|
||||
});
|
||||
|
||||
testWithoutContext('does not rebuild project on subsequent runs', () async {
|
||||
// The first run of 'flutter widget-preview start' should generate a new preview scaffold and
|
||||
// pre-build the application.
|
||||
await runWidgetPreview(expectedMessages: firstLaunchMessages);
|
||||
|
||||
// We shouldn't regenerate the scaffold after the initial run.
|
||||
await runWidgetPreview(expectedMessages: subsequentLaunchMessages);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user