[ Widget Previews ] Add support for detecting previews and generating code (#161911)
`flutter widget-preview start` will now look for functions annotated with `@Preview()` within the developer's project. These functions will be used to generate `.dart_tool/widget_preview_scaffold/lib/generated_preview.dart`, which inserts the returned value from each preview function into a `List<WidgetPreview>` returned from a `previews()` method that is invoked by the widget preview scaffold root. **Example generated_preview.dart:** ```dart // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:foo/foo.dart' as _i1; import 'package:foo/src/bar.dart' as _i2; import 'package:widget_preview/widget_preview.dart'; List<WidgetPreview> previews() => [_i1.preview(), _i2.barPreview1(), _i2.barPreview2()]; ```
This commit is contained in:
parent
a9c50335c7
commit
f25b4b77ff
@ -17,8 +17,11 @@ import '../flutter_manifest.dart';
|
|||||||
import '../globals.dart' as globals;
|
import '../globals.dart' as globals;
|
||||||
import '../project.dart';
|
import '../project.dart';
|
||||||
import '../runner/flutter_command.dart';
|
import '../runner/flutter_command.dart';
|
||||||
|
import '../widget_preview/preview_code_generator.dart';
|
||||||
|
import '../widget_preview/preview_detector.dart';
|
||||||
import 'create_base.dart';
|
import 'create_base.dart';
|
||||||
|
|
||||||
|
// TODO(bkonyi): use dependency injection instead of global accessors throughout this file.
|
||||||
class WidgetPreviewCommand extends FlutterCommand {
|
class WidgetPreviewCommand extends FlutterCommand {
|
||||||
WidgetPreviewCommand() {
|
WidgetPreviewCommand() {
|
||||||
addSubcommand(WidgetPreviewStartCommand());
|
addSubcommand(WidgetPreviewStartCommand());
|
||||||
@ -83,6 +86,13 @@ class WidgetPreviewStartCommand extends FlutterCommand
|
|||||||
@override
|
@override
|
||||||
String get name => 'start';
|
String get name => 'start';
|
||||||
|
|
||||||
|
late final PreviewDetector _previewDetector = PreviewDetector(
|
||||||
|
logger: globals.logger,
|
||||||
|
onChangeDetected: onChangeDetected,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final PreviewCodeGenerator _previewCodeGenerator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
final FlutterProject rootProject = getRootProject();
|
final FlutterProject rootProject = getRootProject();
|
||||||
@ -112,9 +122,26 @@ class WidgetPreviewStartCommand extends FlutterCommand
|
|||||||
);
|
);
|
||||||
await _populatePreviewPubspec(rootProject: rootProject);
|
await _populatePreviewPubspec(rootProject: rootProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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: globals.fs,
|
||||||
|
);
|
||||||
|
|
||||||
|
final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory);
|
||||||
|
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
|
||||||
|
|
||||||
|
await _previewDetector.dispose();
|
||||||
return FlutterCommandResult.success();
|
return FlutterCommandResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onChangeDetected(PreviewMapping previews) {
|
||||||
|
// TODO(bkonyi): perform hot reload
|
||||||
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{
|
static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{
|
||||||
'name': 'flutter_gen',
|
'name': 'flutter_gen',
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
// 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:code_builder/code_builder.dart';
|
||||||
|
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../project.dart';
|
||||||
|
import 'preview_detector.dart';
|
||||||
|
|
||||||
|
/// Generates the Dart source responsible for importing widget previews from the developer's project
|
||||||
|
/// into the widget preview scaffold.
|
||||||
|
class PreviewCodeGenerator {
|
||||||
|
PreviewCodeGenerator({required this.widgetPreviewScaffoldProject, required this.fs});
|
||||||
|
|
||||||
|
final FileSystem fs;
|
||||||
|
|
||||||
|
/// The project for the widget preview scaffold found under `.dart_tool/` in the developer's
|
||||||
|
/// project.
|
||||||
|
final FlutterProject widgetPreviewScaffoldProject;
|
||||||
|
|
||||||
|
static const String generatedPreviewFilePath = 'lib/generated_preview.dart';
|
||||||
|
|
||||||
|
/// Generates code used by the widget preview scaffold based on the preview instances listed in
|
||||||
|
/// [previews].
|
||||||
|
///
|
||||||
|
/// The generated file will contain a single top level function named `previews()` which returns
|
||||||
|
/// a `List<WidgetPreview>` that contains each widget preview defined in [previews].
|
||||||
|
///
|
||||||
|
/// An example of a formatted generated file containing previews from two files could be:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// import 'package:foo/foo.dart' as _i1;
|
||||||
|
/// import 'package:foo/src/bar.dart' as _i2;
|
||||||
|
/// import 'package:widget_preview/widget_preview.dart';
|
||||||
|
///
|
||||||
|
/// List<WidgetPreview> previews() => [
|
||||||
|
/// _i1.fooPreview(),
|
||||||
|
/// _i2.barPreview1(),
|
||||||
|
/// _i3.barPreview2(),
|
||||||
|
/// ];
|
||||||
|
/// ```
|
||||||
|
void populatePreviewsInGeneratedPreviewScaffold(PreviewMapping previews) {
|
||||||
|
final Library lib = Library(
|
||||||
|
(LibraryBuilder b) => b.body.addAll(<Spec>[
|
||||||
|
Directive.import(
|
||||||
|
// TODO(bkonyi): update with actual location in the framework
|
||||||
|
'package:widget_preview/widget_preview.dart',
|
||||||
|
),
|
||||||
|
Method(
|
||||||
|
(MethodBuilder b) =>
|
||||||
|
b
|
||||||
|
..body =
|
||||||
|
literalList(<Object?>[
|
||||||
|
for (final MapEntry<String, List<String>>(
|
||||||
|
key: String path,
|
||||||
|
value: List<String> previewMethods,
|
||||||
|
)
|
||||||
|
in previews.entries) ...<Object?>[
|
||||||
|
for (final String method in previewMethods)
|
||||||
|
refer(method, path).call(<Expression>[]),
|
||||||
|
],
|
||||||
|
]).code
|
||||||
|
..name = 'previews'
|
||||||
|
..returns = refer('List<WidgetPreview>'),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
final DartEmitter emitter = DartEmitter.scoped(useNullSafetySyntax: true);
|
||||||
|
final File generatedPreviewFile = fs.file(
|
||||||
|
widgetPreviewScaffoldProject.directory.uri.resolve(generatedPreviewFilePath),
|
||||||
|
);
|
||||||
|
// TODO(bkonyi): do we want to bother with formatting this?
|
||||||
|
generatedPreviewFile.writeAsStringSync(lib.accept(emitter).toString());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
|
||||||
|
import 'package:analyzer/dart/analysis/analysis_context.dart';
|
||||||
|
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
||||||
|
import 'package:analyzer/dart/analysis/results.dart';
|
||||||
|
import 'package:analyzer/dart/ast/ast.dart';
|
||||||
|
import 'package:analyzer/file_system/physical_file_system.dart';
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
|
import '../base/utils.dart';
|
||||||
|
import '../globals.dart' as globals;
|
||||||
|
import 'preview_code_generator.dart';
|
||||||
|
|
||||||
|
typedef PreviewMapping = Map<String, List<String>>;
|
||||||
|
|
||||||
|
class PreviewDetector {
|
||||||
|
PreviewDetector({required this.logger, required this.onChangeDetected});
|
||||||
|
|
||||||
|
final Logger logger;
|
||||||
|
final void Function(PreviewMapping) onChangeDetected;
|
||||||
|
StreamSubscription<WatchEvent>? _fileWatcher;
|
||||||
|
late final PreviewMapping _pathToPreviews;
|
||||||
|
|
||||||
|
/// Starts listening for changes to Dart sources under [projectRoot] and returns
|
||||||
|
/// the initial [PreviewMapping] for the project.
|
||||||
|
Future<PreviewMapping> initialize(Directory projectRoot) async {
|
||||||
|
// Find the initial set of previews.
|
||||||
|
_pathToPreviews = findPreviewFunctions(projectRoot);
|
||||||
|
|
||||||
|
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();
|
||||||
|
// 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') ||
|
||||||
|
eventPath.endsWith(PreviewCodeGenerator.generatedPreviewFilePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.printStatus('Detected change in $eventPath.');
|
||||||
|
final PreviewMapping filePreviewsMapping = findPreviewFunctions(
|
||||||
|
globals.fs.file(Uri.file(event.path)),
|
||||||
|
);
|
||||||
|
if (filePreviewsMapping.isEmpty && !_pathToPreviews.containsKey(eventPath)) {
|
||||||
|
// No previews found or removed, nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filePreviewsMapping.length > 1) {
|
||||||
|
logger.printWarning('Previews from more than one file were detected!');
|
||||||
|
logger.printWarning('Previews: $filePreviewsMapping');
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
if (filePreviews.isNotEmpty) {
|
||||||
|
final List<String>? currentPreviewsForFile = _pathToPreviews[eventPath];
|
||||||
|
if (filePreviews != currentPreviewsForFile) {
|
||||||
|
_pathToPreviews[eventPath] = filePreviews;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The file previously had previews that were removed.
|
||||||
|
logger.printStatus('Previews removed from $eventPath');
|
||||||
|
_pathToPreviews.remove(eventPath);
|
||||||
|
}
|
||||||
|
onChangeDetected(_pathToPreviews);
|
||||||
|
});
|
||||||
|
// Wait for file watcher to finish initializing, otherwise we might miss changes and cause
|
||||||
|
// tests to flake.
|
||||||
|
await watcher.ready;
|
||||||
|
return _pathToPreviews;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _fileWatcher?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for functions annotated with `@Preview` in the current project.
|
||||||
|
PreviewMapping findPreviewFunctions(FileSystemEntity entity) {
|
||||||
|
final AnalysisContextCollection collection = AnalysisContextCollection(
|
||||||
|
includedPaths: <String>[entity.absolute.path],
|
||||||
|
resourceProvider: PhysicalResourceProvider.INSTANCE,
|
||||||
|
);
|
||||||
|
|
||||||
|
final PreviewMapping previews = PreviewMapping();
|
||||||
|
for (final AnalysisContext context in collection.contexts) {
|
||||||
|
logger.printStatus('Finding previews in ${context.contextRoot.root.path}...');
|
||||||
|
|
||||||
|
for (final String filePath in context.contextRoot.analyzedFiles()) {
|
||||||
|
logger.printTrace('Checking file: $filePath');
|
||||||
|
if (!filePath.endsWith('.dart')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[];
|
||||||
|
for (final SyntacticEntity entity in unit.unit.childEntities) {
|
||||||
|
if (entity is FunctionDeclaration && !entity.name.toString().startsWith('_')) {
|
||||||
|
bool foundPreview = false;
|
||||||
|
for (final Annotation annotation in entity.metadata) {
|
||||||
|
if (annotation.name.name == 'Preview') {
|
||||||
|
// What happens if the annotation is applied multiple times?
|
||||||
|
foundPreview = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundPreview) {
|
||||||
|
logger.printStatus('Found preview at:');
|
||||||
|
logger.printStatus('File path: ${unit.uri}');
|
||||||
|
logger.printStatus('Preview function: ${entity.name}');
|
||||||
|
logger.printStatus('');
|
||||||
|
previewEntries.add(entity.name.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (previewEntries.isNotEmpty) {
|
||||||
|
previews[unit.uri.toString()] = previewEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.printWarning('Unknown library type at $filePath: $lib');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int previewCount = previews.values.fold<int>(
|
||||||
|
0,
|
||||||
|
(int count, List<String> value) => count + value.length,
|
||||||
|
);
|
||||||
|
logger.printStatus('Found $previewCount ${pluralize('preview', previewCount)}.');
|
||||||
|
return previews;
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ dependencies:
|
|||||||
args: 2.6.0
|
args: 2.6.0
|
||||||
dds: 5.0.0
|
dds: 5.0.0
|
||||||
dwds: 24.3.3
|
dwds: 24.3.3
|
||||||
|
code_builder: 4.10.1
|
||||||
completion: 1.0.1
|
completion: 1.0.1
|
||||||
coverage: 1.11.1
|
coverage: 1.11.1
|
||||||
crypto: 3.0.6
|
crypto: 3.0.6
|
||||||
@ -121,4 +122,4 @@ dartdoc:
|
|||||||
# Exclude this package from the hosted API docs.
|
# Exclude this package from the hosted API docs.
|
||||||
nodoc: true
|
nodoc: true
|
||||||
|
|
||||||
# PUBSPEC CHECKSUM: cf3c
|
# PUBSPEC CHECKSUM: a49f
|
||||||
|
@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
|
|||||||
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
import 'package:flutter_tools/src/commands/widget_preview.dart';
|
||||||
import 'package:flutter_tools/src/dart/pub.dart';
|
import 'package:flutter_tools/src/dart/pub.dart';
|
||||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||||
|
import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart';
|
import '../../src/context.dart';
|
||||||
@ -33,8 +34,12 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<String> createRootProject() async {
|
Future<Directory> createRootProject() async {
|
||||||
return createProject(tempDir, arguments: <String>['--pub']);
|
return globals.fs.directory(await createProject(tempDir, arguments: <String>['--pub']));
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory widgetPreviewScaffoldFromRootProject({required Directory rootProject}) {
|
||||||
|
return rootProject.childDirectory('.dart_tool').childDirectory('widget_preview_scaffold');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
|
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
|
||||||
@ -43,28 +48,29 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startWidgetPreview({
|
Future<void> startWidgetPreview({
|
||||||
required String? rootProjectPath,
|
required Directory? rootProject,
|
||||||
List<String>? arguments,
|
List<String>? arguments,
|
||||||
}) async {
|
}) async {
|
||||||
await runWidgetPreviewCommand(<String>[
|
await runWidgetPreviewCommand(<String>[
|
||||||
'start',
|
'start',
|
||||||
...?arguments,
|
...?arguments,
|
||||||
if (rootProjectPath != null) rootProjectPath,
|
if (rootProject != null) rootProject.path,
|
||||||
]);
|
]);
|
||||||
|
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
|
||||||
|
rootProject: rootProject ?? globals.fs.currentDirectory,
|
||||||
|
);
|
||||||
|
expect(widgetPreviewScaffoldDir, exists);
|
||||||
expect(
|
expect(
|
||||||
globals.fs
|
widgetPreviewScaffoldDir.childFile(PreviewCodeGenerator.generatedPreviewFilePath),
|
||||||
.directory(rootProjectPath ?? globals.fs.currentDirectory.path)
|
|
||||||
.childDirectory('.dart_tool')
|
|
||||||
.childDirectory('widget_preview_scaffold'),
|
|
||||||
exists,
|
exists,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cleanWidgetPreview({required String rootProjectPath}) async {
|
Future<void> cleanWidgetPreview({required Directory rootProject}) async {
|
||||||
await runWidgetPreviewCommand(<String>['clean', rootProjectPath]);
|
await runWidgetPreviewCommand(<String>['clean', rootProject.path]);
|
||||||
expect(
|
expect(
|
||||||
globals.fs
|
globals.fs
|
||||||
.directory(rootProjectPath)
|
.directory(rootProject)
|
||||||
.childDirectory('.dart_tool')
|
.childDirectory('.dart_tool')
|
||||||
.childDirectory('widget_preview_scaffold'),
|
.childDirectory('widget_preview_scaffold'),
|
||||||
isNot(exists),
|
isNot(exists),
|
||||||
@ -93,7 +99,7 @@ void main() {
|
|||||||
|
|
||||||
testUsingContext('run outside of a Flutter project directory', () async {
|
testUsingContext('run outside of a Flutter project directory', () async {
|
||||||
try {
|
try {
|
||||||
await startWidgetPreview(rootProjectPath: tempDir.path);
|
await startWidgetPreview(rootProject: tempDir);
|
||||||
fail('Successfully executed outside of a Flutter project directory');
|
fail('Successfully executed outside of a Flutter project directory');
|
||||||
} on ToolExit catch (e) {
|
} on ToolExit catch (e) {
|
||||||
expect(e.message, contains('${tempDir.path} is not a valid Flutter project.'));
|
expect(e.message, contains('${tempDir.path} is not a valid Flutter project.'));
|
||||||
@ -104,8 +110,8 @@ void main() {
|
|||||||
testUsingContext(
|
testUsingContext(
|
||||||
'start creates .dart_tool/widget_preview_scaffold',
|
'start creates .dart_tool/widget_preview_scaffold',
|
||||||
() async {
|
() async {
|
||||||
final String rootProjectPath = await createRootProject();
|
final Directory rootProject = await createRootProject();
|
||||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
await startWidgetPreview(rootProject: rootProject);
|
||||||
},
|
},
|
||||||
overrides: <Type, Generator>{
|
overrides: <Type, Generator>{
|
||||||
Pub:
|
Pub:
|
||||||
@ -123,11 +129,89 @@ void main() {
|
|||||||
testUsingContext(
|
testUsingContext(
|
||||||
'start creates .dart_tool/widget_preview_scaffold in the CWD',
|
'start creates .dart_tool/widget_preview_scaffold in the CWD',
|
||||||
() async {
|
() async {
|
||||||
final String rootProjectPath = await createRootProject();
|
final Directory rootProject = await createRootProject();
|
||||||
await io.IOOverrides.runZoned<Future<void>>(() async {
|
await io.IOOverrides.runZoned<Future<void>>(() async {
|
||||||
// Try to execute using the CWD.
|
// Try to execute using the CWD.
|
||||||
await startWidgetPreview(rootProjectPath: null);
|
await startWidgetPreview(rootProject: null);
|
||||||
}, getCurrentDirectory: () => globals.fs.directory(rootProjectPath));
|
}, getCurrentDirectory: () => rootProject);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
Pub:
|
||||||
|
() => Pub.test(
|
||||||
|
fileSystem: globals.fs,
|
||||||
|
logger: globals.logger,
|
||||||
|
processManager: globals.processManager,
|
||||||
|
usage: globals.flutterUsage,
|
||||||
|
botDetector: globals.botDetector,
|
||||||
|
platform: globals.platform,
|
||||||
|
stdio: mockStdio,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const String samplePreviewFile = '''
|
||||||
|
// This doesn't need to be valid code for testing as long as it has the @Preview() annotation
|
||||||
|
@Preview()
|
||||||
|
WidgetPreview preview() => WidgetPreview();''';
|
||||||
|
|
||||||
|
const String expectedGeneratedFileContents = '''
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [_i1.preview()];''';
|
||||||
|
|
||||||
|
testUsingContext(
|
||||||
|
'start finds existing previews and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
|
||||||
|
() async {
|
||||||
|
final Directory rootProject = await createRootProject();
|
||||||
|
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
|
||||||
|
rootProject: rootProject,
|
||||||
|
);
|
||||||
|
rootProject
|
||||||
|
.childDirectory('lib')
|
||||||
|
.childFile('foo.dart')
|
||||||
|
.writeAsStringSync(samplePreviewFile);
|
||||||
|
|
||||||
|
final File generatedFile = widgetPreviewScaffoldDir.childFile(
|
||||||
|
PreviewCodeGenerator.generatedPreviewFilePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await startWidgetPreview(rootProject: rootProject);
|
||||||
|
expect(generatedFile.readAsStringSync(), expectedGeneratedFileContents);
|
||||||
|
},
|
||||||
|
overrides: <Type, Generator>{
|
||||||
|
Pub:
|
||||||
|
() => Pub.test(
|
||||||
|
fileSystem: globals.fs,
|
||||||
|
logger: globals.logger,
|
||||||
|
processManager: globals.processManager,
|
||||||
|
usage: globals.flutterUsage,
|
||||||
|
botDetector: globals.botDetector,
|
||||||
|
platform: globals.platform,
|
||||||
|
stdio: mockStdio,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testUsingContext(
|
||||||
|
'start finds existing previews in the CWD and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
|
||||||
|
() async {
|
||||||
|
final Directory rootProject = await createRootProject();
|
||||||
|
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
|
||||||
|
rootProject: rootProject,
|
||||||
|
);
|
||||||
|
rootProject
|
||||||
|
.childDirectory('lib')
|
||||||
|
.childFile('foo.dart')
|
||||||
|
.writeAsStringSync(samplePreviewFile);
|
||||||
|
|
||||||
|
final File generatedFile = widgetPreviewScaffoldDir.childFile(
|
||||||
|
PreviewCodeGenerator.generatedPreviewFilePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await io.IOOverrides.runZoned<Future<void>>(() async {
|
||||||
|
// Try to execute using the CWD.
|
||||||
|
await startWidgetPreview(rootProject: null);
|
||||||
|
expect(generatedFile.readAsStringSync(), expectedGeneratedFileContents);
|
||||||
|
}, getCurrentDirectory: () => globals.fs.directory(rootProject));
|
||||||
},
|
},
|
||||||
overrides: <Type, Generator>{
|
overrides: <Type, Generator>{
|
||||||
Pub:
|
Pub:
|
||||||
@ -145,9 +229,9 @@ void main() {
|
|||||||
testUsingContext(
|
testUsingContext(
|
||||||
'clean deletes .dart_tool/widget_preview_scaffold',
|
'clean deletes .dart_tool/widget_preview_scaffold',
|
||||||
() async {
|
() async {
|
||||||
final String rootProjectPath = await createRootProject();
|
final Directory rootProject = await createRootProject();
|
||||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
await startWidgetPreview(rootProject: rootProject);
|
||||||
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
|
await cleanWidgetPreview(rootProject: rootProject);
|
||||||
},
|
},
|
||||||
overrides: <Type, Generator>{
|
overrides: <Type, Generator>{
|
||||||
Pub:
|
Pub:
|
||||||
@ -166,12 +250,12 @@ void main() {
|
|||||||
'invokes pub in online and offline modes',
|
'invokes pub in online and offline modes',
|
||||||
() async {
|
() async {
|
||||||
// Run pub online first in order to populate the pub cache.
|
// Run pub online first in order to populate the pub cache.
|
||||||
final String rootProjectPath = await createRootProject();
|
final Directory rootProject = await createRootProject();
|
||||||
loggingProcessManager.clear();
|
loggingProcessManager.clear();
|
||||||
|
|
||||||
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
|
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
|
||||||
|
|
||||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
await startWidgetPreview(rootProject: rootProject);
|
||||||
expect(
|
expect(
|
||||||
loggingProcessManager.commands,
|
loggingProcessManager.commands,
|
||||||
contains(
|
contains(
|
||||||
@ -182,12 +266,12 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
|
await cleanWidgetPreview(rootProject: rootProject);
|
||||||
|
|
||||||
// Run pub offline.
|
// Run pub offline.
|
||||||
loggingProcessManager.clear();
|
loggingProcessManager.clear();
|
||||||
await startWidgetPreview(
|
await startWidgetPreview(
|
||||||
rootProjectPath: rootProjectPath,
|
rootProject: rootProject,
|
||||||
arguments: <String>['--pub', '--offline'],
|
arguments: <String>['--pub', '--offline'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
// 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/memory.dart';
|
||||||
|
import 'package:file_testing/file_testing.dart';
|
||||||
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
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:test/test.dart';
|
||||||
|
|
||||||
|
import '../../src/context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('$PreviewCodeGenerator', () {
|
||||||
|
late PreviewCodeGenerator codeGenerator;
|
||||||
|
late FlutterProject project;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
final FileSystem fs = MemoryFileSystem.test();
|
||||||
|
final FlutterManifest manifest = FlutterManifest.empty(logger: BufferLogger.test());
|
||||||
|
final Directory projectDir =
|
||||||
|
fs.currentDirectory.childDirectory('project')
|
||||||
|
..createSync()
|
||||||
|
..childDirectory('lib').createSync();
|
||||||
|
project = FlutterProject(projectDir, manifest, manifest);
|
||||||
|
codeGenerator = PreviewCodeGenerator(widgetPreviewScaffoldProject: project, fs: fs);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext(
|
||||||
|
'correctly generates ${PreviewCodeGenerator.generatedPreviewFilePath}',
|
||||||
|
() async {
|
||||||
|
// Check that the generated preview file doesn't exist yet.
|
||||||
|
final File generatedPreviewFile = project.directory.childFile(
|
||||||
|
PreviewCodeGenerator.generatedPreviewFilePath,
|
||||||
|
);
|
||||||
|
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'],
|
||||||
|
});
|
||||||
|
expect(generatedPreviewFile, exists);
|
||||||
|
|
||||||
|
// Check that the generated file contains:
|
||||||
|
// - An import of the widget preview library
|
||||||
|
// - Prefixed imports for both 'foo.dart' and 'src/bar.dart'
|
||||||
|
// - A top-level function 'List<WidgetPreview> previews()'
|
||||||
|
// - A returned list containing function calls to 'preview()' from 'foo.dart' and 'barPreview1()'
|
||||||
|
// and 'barPreview2()' from 'src/bar.dart'
|
||||||
|
//
|
||||||
|
// The generated file is unfortunately unformatted.
|
||||||
|
const String expectedGeneratedPreviewFileContents = '''
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [_i1.preview(), _i2.barPreview1(), _i2.barPreview2(), ];''';
|
||||||
|
expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents);
|
||||||
|
|
||||||
|
// Regenerate the generated file with no previews.
|
||||||
|
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{});
|
||||||
|
expect(generatedPreviewFile, exists);
|
||||||
|
|
||||||
|
// The generated file should only contain:
|
||||||
|
// - An import of the widget preview library
|
||||||
|
// - A top-level function 'List<WidgetPreview> previews()' that returns an empty list.
|
||||||
|
const String emptyGeneratedPreviewFileContents = '''
|
||||||
|
import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [];''';
|
||||||
|
expect(generatedPreviewFile.readAsStringSync(), emptyGeneratedPreviewFileContents);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// 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:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
|
import 'package:flutter_tools/src/base/signals.dart';
|
||||||
|
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../../src/common.dart';
|
||||||
|
import '../../src/context.dart';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
File addNonPreviewContainingFile(Directory projectRoot, String path) {
|
||||||
|
return projectRoot.childDirectory('lib').childFile(path)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync(nonPreviewContainingFileContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('$PreviewDetector', () {
|
||||||
|
// Note: we don't use a MemoryFileSystem since we don't have a way to
|
||||||
|
// provide it to package:analyzer APIs without writing a significant amount
|
||||||
|
// of wrapper logic.
|
||||||
|
late LocalFileSystem fs;
|
||||||
|
late Logger logger;
|
||||||
|
late PreviewDetector previewDetector;
|
||||||
|
late Directory projectRoot;
|
||||||
|
void Function(PreviewMapping)? onChangeDetected;
|
||||||
|
|
||||||
|
void onChangeDetectedRoot(PreviewMapping mapping) {
|
||||||
|
onChangeDetected!(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
fs = LocalFileSystem.test(signals: Signals.test());
|
||||||
|
projectRoot = createBasicProjectStructure(fs);
|
||||||
|
logger = BufferLogger.test();
|
||||||
|
previewDetector = PreviewDetector(logger: logger, onChangeDetected: onChangeDetectedRoot);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await previewDetector.dispose();
|
||||||
|
projectRoot.deleteSync(recursive: true);
|
||||||
|
onChangeDetected = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('can detect previews in existing files', () async {
|
||||||
|
final List<File> previewFiles = <File>[
|
||||||
|
addPreviewContainingFile(projectRoot, 'foo.dart'),
|
||||||
|
addPreviewContainingFile(projectRoot, 'src/bar.dart'),
|
||||||
|
];
|
||||||
|
addNonPreviewContainingFile(projectRoot, 'baz.dart');
|
||||||
|
final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot);
|
||||||
|
expect(mapping.keys.toSet(), previewFiles.map((File e) => e.uri.toString()).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 File nonPreviewContainingFile = addNonPreviewContainingFile(projectRoot, '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>>{
|
||||||
|
...expectedInitialMapping,
|
||||||
|
nonPreviewContainingFile.uri.toString(): <String>['previews'],
|
||||||
|
});
|
||||||
|
completer.complete();
|
||||||
|
};
|
||||||
|
// Initialize the file watcher.
|
||||||
|
final PreviewMapping initialPreviews = await previewDetector.initialize(projectRoot);
|
||||||
|
expect(initialPreviews, expectedInitialMapping);
|
||||||
|
|
||||||
|
// Update the file without an existing preview to include a preview and ensure it triggers
|
||||||
|
// the preview detector.
|
||||||
|
addPreviewContainingFile(projectRoot, 'baz.dart');
|
||||||
|
await completer.future;
|
||||||
|
|
||||||
|
completer = Completer<void>();
|
||||||
|
onChangeDetected = (PreviewMapping updated) {
|
||||||
|
// The removed preview in baz.dart should not longer be included in the preview mapping.
|
||||||
|
expect(updated, expectedInitialMapping);
|
||||||
|
completer.complete();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the file with an existing preview to remove the preview and ensure it triggers
|
||||||
|
// the preview detector.
|
||||||
|
addNonPreviewContainingFile(projectRoot, 'baz.dart');
|
||||||
|
await completer.future;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const String previewContainingFileContents = '''
|
||||||
|
@Preview()
|
||||||
|
// This isn't necessarily valid code. We're just looking for the annotation
|
||||||
|
WidgetPreview previews() => WidgetPreview();
|
||||||
|
''';
|
||||||
|
|
||||||
|
const String nonPreviewContainingFileContents = '''
|
||||||
|
String foo() => 'bar';
|
||||||
|
''';
|
Loading…
x
Reference in New Issue
Block a user