forked from firka/flutter
[ Widget Preview ] Add experimental support for web-based widget preview environment (#163154)
`flutter widget-preview start --web` will cause the widget preview scaffold to be run as a Flutter Web application using experimental hot reload support. This will eventually be the default, with the desktop environment being put behind a flag for use as a fallback under the assumption that the desktop environment will be removed in the future.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
|
||||
import '../base/common.dart';
|
||||
import '../base/deferred_component.dart';
|
||||
@@ -118,17 +119,36 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
required this.os,
|
||||
}) {
|
||||
addPubOptions();
|
||||
argParser.addFlag(
|
||||
kLaunchPreviewer,
|
||||
defaultsTo: true,
|
||||
help: 'Launches the widget preview environment.',
|
||||
// Should only be used for testing.
|
||||
hide: !verboseHelp,
|
||||
);
|
||||
argParser
|
||||
..addFlag(
|
||||
kLaunchPreviewer,
|
||||
defaultsTo: true,
|
||||
help: 'Launches the widget preview environment.',
|
||||
// Should only be used for testing.
|
||||
hide: !verboseHelp,
|
||||
)
|
||||
..addFlag(
|
||||
kUseFlutterWeb,
|
||||
help: 'Launches the widget preview environment using Flutter Web.',
|
||||
hide: !verboseHelp,
|
||||
)
|
||||
..addFlag(
|
||||
kHeadlessWeb,
|
||||
help: 'Launches Chrome in headless mode for testing.',
|
||||
hide: !verboseHelp,
|
||||
);
|
||||
}
|
||||
|
||||
static const String kWidgetPreviewScaffoldName = 'widget_preview_scaffold';
|
||||
static const String kLaunchPreviewer = 'launch-previewer';
|
||||
static const String kUseFlutterWeb = 'web';
|
||||
static const String kHeadlessWeb = 'headless-web';
|
||||
|
||||
@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.';
|
||||
@@ -138,6 +158,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
final bool verboseHelp;
|
||||
|
||||
bool get isWeb => boolArg(kUseFlutterWeb);
|
||||
|
||||
@override
|
||||
final FileSystem fs;
|
||||
|
||||
@@ -188,9 +210,10 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
titleCaseProjectName: 'Widget Preview Scaffold',
|
||||
flutterRoot: Cache.flutterRoot!,
|
||||
dartSdkVersionBounds: '^${cache.dartSdkBuild}',
|
||||
linux: platform.isLinux,
|
||||
macos: platform.isMacOS,
|
||||
windows: platform.isWindows,
|
||||
linux: platform.isLinux && !isWeb,
|
||||
macos: platform.isMacOS && !isWeb,
|
||||
windows: platform.isWindows && !isWeb,
|
||||
web: isWeb,
|
||||
),
|
||||
overwrite: true,
|
||||
generateMetadata: false,
|
||||
@@ -238,6 +261,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
}
|
||||
|
||||
void onChangeDetected(PreviewMapping previews) {
|
||||
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(previews);
|
||||
logger.printStatus('Triggering reload based on change to preview set: $previews');
|
||||
_widgetPreviewApp?.restart();
|
||||
}
|
||||
@@ -250,9 +274,10 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
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,
|
||||
linuxPlatform: platform.isLinux && !isWeb,
|
||||
macOSPlatform: platform.isMacOS && !isWeb,
|
||||
windowsPlatform: platform.isWindows && !isWeb,
|
||||
webPlatform: isWeb,
|
||||
);
|
||||
|
||||
// Generate initial package_config.json, otherwise the build will fail.
|
||||
@@ -263,6 +288,10 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
outputMode: PubOutputMode.summaryOnly,
|
||||
);
|
||||
|
||||
if (isWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
|
||||
logger.printStatus('Performing initial build of the Widget Preview Scaffold...');
|
||||
|
||||
@@ -362,9 +391,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
// 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),
|
||||
);
|
||||
File? prebuiltApplicationBinary;
|
||||
if (!isWeb) {
|
||||
prebuiltApplicationBinary = fs.file(
|
||||
prebuiltApplicationBinaryPath(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject),
|
||||
);
|
||||
}
|
||||
const String? kEmptyRoute = null;
|
||||
const bool kEnableHotReload = true;
|
||||
|
||||
@@ -378,12 +410,20 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
BuildMode.debug,
|
||||
null,
|
||||
treeShakeIcons: false,
|
||||
extraFrontEndOptions:
|
||||
isWeb ? <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null,
|
||||
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
|
||||
packageConfig: PackageConfig.parseBytes(
|
||||
widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),
|
||||
widgetPreviewScaffoldProject.packageConfig.uri,
|
||||
),
|
||||
),
|
||||
webEnableExposeUrl: false, // TODO(bkonyi): verify
|
||||
webRunHeadless: boolArg(kHeadlessWeb),
|
||||
),
|
||||
kEnableHotReload, // hot mode
|
||||
applicationBinary: prebuiltApplicationBinary,
|
||||
trackWidgetCreation: false,
|
||||
trackWidgetCreation: true,
|
||||
projectRootPath: widgetPreviewScaffoldProject.directory.path,
|
||||
);
|
||||
} on Exception catch (error) {
|
||||
|
||||
@@ -71,7 +71,9 @@ class WebBuilder {
|
||||
)).any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
|
||||
final Directory outputDirectory =
|
||||
outputDirectoryPath == null
|
||||
? _fileSystem.directory(getWebBuildDirectory())
|
||||
? _fileSystem.directory(
|
||||
_fileSystem.path.join(flutterProject.directory.path, getWebBuildDirectory()),
|
||||
)
|
||||
: _fileSystem.directory(outputDirectoryPath);
|
||||
outputDirectory.createSync(recursive: true);
|
||||
|
||||
@@ -89,7 +91,7 @@ class WebBuilder {
|
||||
final BuildResult result = await _buildSystem.build(
|
||||
globals.buildTargets.webServiceWorker(_fileSystem, compilerConfigs),
|
||||
Environment(
|
||||
projectDir: _fileSystem.currentDirectory,
|
||||
projectDir: flutterProject.directory,
|
||||
outputDir: outputDirectory,
|
||||
buildDir: flutterProject.directory
|
||||
.childDirectory('.dart_tool')
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'src/widget_preview_rendering.dart';
|
||||
import 'src/generated_preview.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await mainImpl(previewsProvider: previews);
|
||||
await mainImpl();
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'controls.dart';
|
||||
import 'generated_preview.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class WidgetPreviewWidget extends StatefulWidget {
|
||||
@@ -289,6 +291,10 @@ class PreviewAssetBundle extends PlatformAssetBundle {
|
||||
|
||||
@override
|
||||
Future<ImmutableBuffer> loadBuffer(String key) async {
|
||||
if (kIsWeb) {
|
||||
final ByteData bytes = await load(key);
|
||||
return ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes));
|
||||
}
|
||||
return await ImmutableBuffer.fromAsset(
|
||||
key.startsWith(_kPackagesPrefix) ? key : '../../$key',
|
||||
);
|
||||
@@ -300,24 +306,18 @@ class PreviewAssetBundle extends PlatformAssetBundle {
|
||||
/// We don't actually define this as `main` to avoid copying this file into
|
||||
/// the preview scaffold project which prevents us from being able to use hot
|
||||
/// restart to iterate on this file.
|
||||
Future<void> mainImpl({
|
||||
required List<WidgetPreview> Function() previewsProvider,
|
||||
}) async {
|
||||
Future<void> mainImpl() async {
|
||||
runApp(
|
||||
_WidgetPreviewScaffold(
|
||||
previewsProvider: previewsProvider,
|
||||
),
|
||||
_WidgetPreviewScaffold(),
|
||||
);
|
||||
}
|
||||
|
||||
class _WidgetPreviewScaffold extends StatelessWidget {
|
||||
const _WidgetPreviewScaffold({required this.previewsProvider});
|
||||
|
||||
final List<WidgetPreview> Function() previewsProvider;
|
||||
const _WidgetPreviewScaffold();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<WidgetPreview> previewList = previewsProvider();
|
||||
final List<WidgetPreview> previewList = previews();
|
||||
Widget previewView;
|
||||
if (previewList.isEmpty) {
|
||||
previewView = const Column(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
@@ -26,6 +27,12 @@ const List<String> subsequentLaunchMessages = <String>[
|
||||
'Done loading previews.',
|
||||
];
|
||||
|
||||
const List<String> firstLaunchMessagesWeb = <String>[
|
||||
'Creating widget preview scaffolding at:',
|
||||
'Loading previews into the Widget Preview Scaffold...',
|
||||
'Done loading previews.',
|
||||
];
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
Process? process;
|
||||
@@ -43,13 +50,21 @@ void main() {
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
Future<void> runWidgetPreview({required List<String> expectedMessages}) async {
|
||||
Future<void> runWidgetPreview({
|
||||
required List<String> expectedMessages,
|
||||
bool useWeb = false,
|
||||
}) async {
|
||||
expect(expectedMessages, isNotEmpty);
|
||||
int i = 0;
|
||||
process = await processManager.start(<String>[
|
||||
flutterBin,
|
||||
'widget-preview',
|
||||
'start',
|
||||
'--verbose',
|
||||
if (useWeb) ...<String>[
|
||||
'--${WidgetPreviewStartCommand.kUseFlutterWeb}',
|
||||
'--${WidgetPreviewStartCommand.kHeadlessWeb}',
|
||||
],
|
||||
], workingDirectory: tempDir.path);
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
@@ -70,6 +85,16 @@ void main() {
|
||||
printOnFailure('STDERR: $msg');
|
||||
});
|
||||
|
||||
unawaited(
|
||||
process!.exitCode.then((int exitCode) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
completer.completeError(
|
||||
TestFailure('The widget previewer exited unexpectedly (exit code: $exitCode)'),
|
||||
);
|
||||
}),
|
||||
);
|
||||
await completer.future;
|
||||
process!.kill();
|
||||
process = null;
|
||||
@@ -80,6 +105,10 @@ void main() {
|
||||
await runWidgetPreview(expectedMessages: firstLaunchMessages);
|
||||
});
|
||||
|
||||
testWithoutContext('web smoke test', () async {
|
||||
await runWidgetPreview(expectedMessages: firstLaunchMessagesWeb, useWeb: true);
|
||||
});
|
||||
|
||||
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.
|
||||
@@ -88,5 +117,13 @@ void main() {
|
||||
// We shouldn't regenerate the scaffold after the initial run.
|
||||
await runWidgetPreview(expectedMessages: subsequentLaunchMessages);
|
||||
});
|
||||
|
||||
testWithoutContext('does not recreate project on subsequent --web runs', () async {
|
||||
// The first run of 'flutter widget-preview start --web' should generate a new preview scaffold
|
||||
await runWidgetPreview(expectedMessages: firstLaunchMessagesWeb, useWeb: true);
|
||||
|
||||
// We shouldn't regenerate the scaffold after the initial run.
|
||||
await runWidgetPreview(expectedMessages: subsequentLaunchMessages, useWeb: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user