[ Widget Preview ] Invalidate scaffold project if SDK changes and regenerate pubspec on change (#163343)
If the Flutter SDK is updated we need to invalidate the widget preview scaffold project to, at the very least, rebuild the precompiled application. Similarly, if the root project's `pubspec.yaml` is updated, the scaffold project's pubspec should be regenerated. This change adds a `preview_manifest.json` to the scaffold project which contains information related to: - The manifest schema version - The Dart SDK version the project was generated with - The last known hash of the root project's pubspec.yaml This information is used to determine whether or not the scaffold project needs to be regenerated or the scaffold pubspec needs to be updated to reflect changes to the root project's pubspec since the previewer was last run.
This commit is contained in:
parent
78d0f5f0f6
commit
cccb49d3e6
@ -26,6 +26,7 @@ import '../project.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
import '../widget_preview/preview_code_generator.dart';
|
||||
import '../widget_preview/preview_detector.dart';
|
||||
import '../widget_preview/preview_manifest.dart';
|
||||
import '../windows/build_windows.dart';
|
||||
import 'create_base.dart';
|
||||
import 'daemon.dart';
|
||||
@ -177,25 +178,35 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
|
||||
final OperatingSystemUtils os;
|
||||
|
||||
late final FlutterProject rootProject = getRootProject();
|
||||
|
||||
late final PreviewDetector _previewDetector = PreviewDetector(
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
onChangeDetected: onChangeDetected,
|
||||
onPubspecChangeDetected: onPubspecChangeDetected,
|
||||
);
|
||||
|
||||
late final PreviewCodeGenerator _previewCodeGenerator;
|
||||
late final PreviewManifest _previewManifest;
|
||||
|
||||
/// The currently running instance of the widget preview scaffold.
|
||||
AppInstance? _widgetPreviewApp;
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
final FlutterProject rootProject = getRootProject();
|
||||
final Directory widgetPreviewScaffold = rootProject.widgetPreviewScaffold;
|
||||
_previewManifest = PreviewManifest(
|
||||
logger: logger,
|
||||
rootProject: rootProject,
|
||||
fs: fs,
|
||||
cache: cache,
|
||||
);
|
||||
|
||||
// Check to see if a preview scaffold has already been generated. If not,
|
||||
// generate one.
|
||||
final bool generateScaffoldProject = !widgetPreviewScaffold.existsSync();
|
||||
final bool generateScaffoldProject = _previewManifest.shouldGenerateProject();
|
||||
// TODO(bkonyi): can this be moved?
|
||||
widgetPreviewScaffold.createSync();
|
||||
|
||||
if (generateScaffoldProject) {
|
||||
@ -218,6 +229,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
overwrite: true,
|
||||
generateMetadata: false,
|
||||
);
|
||||
_previewManifest.generate();
|
||||
|
||||
// WARNING: this access of widgetPreviewScaffoldProject needs to happen
|
||||
// after we generate the scaffold project as invoking the getter triggers
|
||||
@ -232,13 +244,21 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
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);
|
||||
if (generateScaffoldProject || _previewManifest.shouldRegeneratePubspec()) {
|
||||
if (!generateScaffoldProject) {
|
||||
logger.printStatus(
|
||||
'Detected changes in pubspec.yaml. Regenerating pubspec.yaml for the '
|
||||
'widget preview scaffold.',
|
||||
);
|
||||
}
|
||||
// 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);
|
||||
@ -266,6 +286,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
_widgetPreviewApp?.restart();
|
||||
}
|
||||
|
||||
void onPubspecChangeDetected() {
|
||||
// TODO(bkonyi): trigger hot reload or restart?
|
||||
logger.printStatus('Changes to pubspec.yaml detected.');
|
||||
_populatePreviewPubspec(rootProject: rootProject);
|
||||
}
|
||||
|
||||
/// Builds the application binary for the widget preview scaffold the first
|
||||
/// time the widget preview command is run.
|
||||
///
|
||||
@ -566,6 +592,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
);
|
||||
|
||||
maybeAddFlutterGenToPackageConfig(rootProject: rootProject);
|
||||
_previewManifest.updatePubspecHash();
|
||||
}
|
||||
|
||||
/// Manually adds an entry for package:flutter_gen to the preview scaffold's
|
||||
|
@ -40,6 +40,8 @@ extension on Annotation {
|
||||
/// Convenience getters for examining [String] paths.
|
||||
extension on String {
|
||||
bool get isDartFile => endsWith('.dart');
|
||||
bool get isPubspec => endsWith('pubspec.yaml');
|
||||
bool get doesContainDartTool => contains('.dart_tool');
|
||||
bool get isGeneratedPreviewFile => endsWith(PreviewCodeGenerator.generatedPreviewFilePath);
|
||||
}
|
||||
|
||||
@ -49,11 +51,18 @@ extension on ParsedUnitResult {
|
||||
}
|
||||
|
||||
class PreviewDetector {
|
||||
PreviewDetector({required this.fs, required this.logger, required this.onChangeDetected});
|
||||
PreviewDetector({
|
||||
required this.fs,
|
||||
required this.logger,
|
||||
required this.onChangeDetected,
|
||||
required this.onPubspecChangeDetected,
|
||||
});
|
||||
|
||||
final FileSystem fs;
|
||||
final Logger logger;
|
||||
final void Function(PreviewMapping) onChangeDetected;
|
||||
final void Function() onPubspecChangeDetected;
|
||||
|
||||
StreamSubscription<WatchEvent>? _fileWatcher;
|
||||
late final PreviewMapping _pathToPreviews;
|
||||
|
||||
@ -64,9 +73,14 @@ class PreviewDetector {
|
||||
_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 = event.path;
|
||||
// If the pubspec has changed, new dependencies or assets could have been added, requiring
|
||||
// the preview scaffold's pubspec to be updated.
|
||||
if (eventPath.isPubspec && !eventPath.doesContainDartTool) {
|
||||
onPubspecChangeDetected();
|
||||
return;
|
||||
}
|
||||
// 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.isDartFile || eventPath.isGeneratedPreviewFile) {
|
||||
|
@ -0,0 +1,137 @@
|
||||
// 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:crypto/crypto.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/version.dart';
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../project.dart';
|
||||
|
||||
typedef PreviewManifestContents = Map<String, Object?>;
|
||||
|
||||
class PreviewManifest {
|
||||
PreviewManifest({
|
||||
required this.logger,
|
||||
required this.rootProject,
|
||||
required this.fs,
|
||||
required this.cache,
|
||||
});
|
||||
|
||||
static const String previewManifestPath = 'preview_manifest.json';
|
||||
static final Version previewManifestVersion = Version(0, 0, 1);
|
||||
static const String kManifestVersion = 'version';
|
||||
static const String kSdkVersion = 'sdk-version';
|
||||
static const String kPubspecHash = 'pubspec-hash';
|
||||
|
||||
final Logger logger;
|
||||
final FlutterProject rootProject;
|
||||
final FileSystem fs;
|
||||
final Cache cache;
|
||||
|
||||
Directory get widgetPreviewScaffold => rootProject.widgetPreviewScaffold;
|
||||
String get _manifestPath => fs.path.join(widgetPreviewScaffold.path, previewManifestPath);
|
||||
File get _manifest => fs.file(_manifestPath);
|
||||
|
||||
PreviewManifestContents? _tryLoadManifest() {
|
||||
final File manifest = fs.file(_manifestPath);
|
||||
if (!manifest.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
return json.decode(manifest.readAsStringSync()) as PreviewManifestContents;
|
||||
}
|
||||
|
||||
void generate() {
|
||||
logger.printStatus('Creating the Widget Preview Scaffold manifest at ${_manifest.path}');
|
||||
assert(!_manifest.existsSync());
|
||||
_manifest.createSync(recursive: true);
|
||||
final PreviewManifestContents manifestContents = <String, Object?>{
|
||||
kManifestVersion: previewManifestVersion.toString(),
|
||||
kSdkVersion: cache.dartSdkVersion,
|
||||
kPubspecHash: _calculatePubspecHash(),
|
||||
};
|
||||
_updateManifest(manifestContents);
|
||||
}
|
||||
|
||||
void _updateManifest(PreviewManifestContents contents) {
|
||||
_manifest.writeAsStringSync(json.encode(contents));
|
||||
}
|
||||
|
||||
String _calculatePubspecHash() {
|
||||
return md5.convert(rootProject.manifest.toYaml().toString().codeUnits).toString();
|
||||
}
|
||||
|
||||
bool shouldGenerateProject() {
|
||||
if (!widgetPreviewScaffold.existsSync()) {
|
||||
return true;
|
||||
}
|
||||
final PreviewManifestContents? manifest = _tryLoadManifest();
|
||||
// If the manifest doesn't exist or the SDK version isn't present, the widget preview scaffold
|
||||
// should be regenerated and rebuilt.
|
||||
if (manifest == null ||
|
||||
!manifest.containsKey(kManifestVersion) ||
|
||||
!manifest.containsKey(kSdkVersion)) {
|
||||
logger.printWarning(
|
||||
'Invalid Widget Preview Scaffold manifest at ${_manifest.path}. Regenerating Widget '
|
||||
'Preview Scaffold.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
final Version? manifestVersion = Version.parse(manifest[kManifestVersion]! as String);
|
||||
// If the manifest version in the scaffold doesn't match the current preview manifest spec,
|
||||
// we should regenerate it.
|
||||
// TODO(bkonyi): is this actually what we want to do, or do we want to just update the manifest?
|
||||
if (manifestVersion == null || manifestVersion != previewManifestVersion) {
|
||||
logger.printStatus(
|
||||
'The existing Widget Preview Scaffold manifest version ($manifestVersion) '
|
||||
'is older than the currently supported version ($previewManifestVersion). Regenerating '
|
||||
'Widget Preview Scaffold.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// If the SDK version of the widget preview scaffold doesn't match the current SDK version
|
||||
// the widget preview scaffold should also be regenerated to pick up any new functionality and
|
||||
// avoid possible binary compatibility issues.
|
||||
final bool sdkVersionMismatch = manifest[kSdkVersion] != cache.dartSdkVersion;
|
||||
if (sdkVersionMismatch) {
|
||||
logger.printStatus(
|
||||
'The existing Widget Preview Scaffold was generated with Dart SDK '
|
||||
'version ${manifest[kSdkVersion]}, which does not match the current Dart SDK version '
|
||||
'(${cache.dartSdkVersion}). Regenerating Widget Preview Scaffold.',
|
||||
);
|
||||
}
|
||||
return sdkVersionMismatch;
|
||||
}
|
||||
|
||||
bool shouldRegeneratePubspec() {
|
||||
final PreviewManifestContents manifest = _tryLoadManifest()!;
|
||||
if (!manifest.containsKey(kPubspecHash)) {
|
||||
logger.printWarning(
|
||||
'The Widget Preview Scaffold manifest does not include the last known state of the root '
|
||||
"project's pubspec.yaml.",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return manifest[kPubspecHash] != _calculatePubspecHash();
|
||||
}
|
||||
|
||||
void updatePubspecHash() {
|
||||
final PreviewManifestContents manifest = _tryLoadManifest()!;
|
||||
manifest[kPubspecHash] = _calculatePubspecHash();
|
||||
_updateManifest(manifest);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
PreviewManifest copyWith({Cache? cache}) {
|
||||
return PreviewManifest(
|
||||
logger: logger,
|
||||
rootProject: rootProject,
|
||||
fs: fs,
|
||||
cache: cache ?? this.cache,
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,12 @@ Directory createBasicProjectStructure(FileSystem fs) {
|
||||
return fs.systemTempDirectory.createTempSync('root');
|
||||
}
|
||||
|
||||
void populatePubspec(Directory projectRoot, String contents) {
|
||||
projectRoot.childFile('pubspec.yaml')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(contents);
|
||||
}
|
||||
|
||||
PreviewPath addPreviewContainingFile(Directory projectRoot, List<String> path) {
|
||||
final File file =
|
||||
projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator))
|
||||
@ -44,11 +50,16 @@ void main() {
|
||||
late PreviewDetector previewDetector;
|
||||
late Directory projectRoot;
|
||||
void Function(PreviewMapping)? onChangeDetected;
|
||||
void Function()? onPubspecChangeDetected;
|
||||
|
||||
void onChangeDetectedRoot(PreviewMapping mapping) {
|
||||
onChangeDetected!(mapping);
|
||||
}
|
||||
|
||||
void onPubspecChangeDetectedRoot() {
|
||||
onPubspecChangeDetected!();
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
fs = LocalFileSystem.test(signals: Signals.test());
|
||||
projectRoot = createBasicProjectStructure(fs);
|
||||
@ -57,6 +68,7 @@ void main() {
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
onChangeDetected: onChangeDetectedRoot,
|
||||
onPubspecChangeDetected: onPubspecChangeDetectedRoot,
|
||||
);
|
||||
});
|
||||
|
||||
@ -117,6 +129,23 @@ void main() {
|
||||
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
await completer.future;
|
||||
});
|
||||
|
||||
testUsingContext('can detect changes in the pubspec.yaml', () async {
|
||||
// Create an initial pubspec.
|
||||
populatePubspec(projectRoot, 'abc');
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
onPubspecChangeDetected = () {
|
||||
completer.complete();
|
||||
};
|
||||
// Initialize the file watcher.
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize(projectRoot);
|
||||
expect(initialPreviews, isEmpty);
|
||||
|
||||
// Change the contents of the pubspec and verify the callback is invoked.
|
||||
populatePubspec(projectRoot, 'foo');
|
||||
await completer.future;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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:convert';
|
||||
|
||||
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/platform.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/flutter_manifest.dart';
|
||||
import 'package:flutter_tools/src/project.dart';
|
||||
import 'package:flutter_tools/src/widget_preview/preview_manifest.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart';
|
||||
|
||||
void main() {
|
||||
group('$PreviewManifest', () {
|
||||
late FlutterProject rootProject;
|
||||
late PreviewManifest previewManifest;
|
||||
late Logger logger;
|
||||
|
||||
// The version really doesn't matter, just the format.
|
||||
const String kFakeSDKVersion = '2.1.0-dev.8.0.flutter-4312ae32';
|
||||
|
||||
setUp(() {
|
||||
final MemoryFileSystem fs = MemoryFileSystem.test();
|
||||
final FlutterManifest manifest = FlutterManifest.empty(logger: BufferLogger.test());
|
||||
final Directory projectDir = fs.currentDirectory.childDirectory('project')..createSync();
|
||||
projectDir.childDirectory('lib/src').createSync(recursive: true);
|
||||
rootProject = FlutterProject(projectDir, manifest, manifest);
|
||||
logger = BufferLogger.test();
|
||||
previewManifest = PreviewManifest(
|
||||
logger: logger,
|
||||
rootProject: rootProject,
|
||||
fs: fs,
|
||||
cache: Cache.test(
|
||||
processManager: FakeProcessManager.any(),
|
||||
fileSystem: fs,
|
||||
platform: FakePlatform(version: kFakeSDKVersion),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testUsingContext('generates a valid manifest', () async {
|
||||
previewManifest.generate();
|
||||
final PreviewManifestContents manifest =
|
||||
json.decode(
|
||||
rootProject.widgetPreviewScaffold
|
||||
.childFile(PreviewManifest.previewManifestPath)
|
||||
.readAsStringSync(),
|
||||
)
|
||||
as PreviewManifestContents;
|
||||
|
||||
expect(manifest.containsKey(PreviewManifest.kPubspecHash), true);
|
||||
expect(manifest.containsKey(PreviewManifest.kManifestVersion), true);
|
||||
expect(manifest.containsKey(PreviewManifest.kSdkVersion), true);
|
||||
});
|
||||
|
||||
testUsingContext('identifies widget preview scaffold project needs to be generated', () {
|
||||
// The widget preview scaffold directory doesn't exist, so we should know that we need to
|
||||
// generate the project.
|
||||
expect(previewManifest.shouldGenerateProject(), true);
|
||||
|
||||
// Populate the manifest. For this test, this has the side effect of creating the widget
|
||||
// preview scaffold project directory as well.
|
||||
previewManifest.generate();
|
||||
|
||||
// The widget preview scaffold project directory should exist as well as the newly generated
|
||||
// preview manifest.
|
||||
expect(previewManifest.shouldGenerateProject(), false);
|
||||
|
||||
// Simulate changing the SDK version and verify that we should regenerate the project.
|
||||
final PreviewManifest modified = previewManifest.copyWith(
|
||||
cache: Cache.test(
|
||||
processManager: FakeProcessManager.any(),
|
||||
platform: FakePlatform(version: '${kFakeSDKVersion}foo'),
|
||||
),
|
||||
);
|
||||
|
||||
const String sdkMismatchMessage =
|
||||
'The existing Widget Preview Scaffold was generated with Dart SDK '
|
||||
'version 2.1.0 (build 2.1.0-dev.8.0 4312ae32), which does not match the current Dart '
|
||||
'SDK version (2.1.0 (build 2.1.0-dev.8.0 4312ae32foo)). Regenerating Widget Preview '
|
||||
'Scaffold.\n';
|
||||
expect(modified.shouldGenerateProject(), true);
|
||||
expect((modified.logger as BufferLogger).statusText, contains(sdkMismatchMessage));
|
||||
});
|
||||
|
||||
testUsingContext('identifies root project pubspec has changed', () {
|
||||
// The widget preview scaffold directory doesn't exist, so we should know that we need to
|
||||
// generate the project.
|
||||
expect(previewManifest.shouldGenerateProject(), true);
|
||||
|
||||
// Populate the manifest. For this test, this has the side effect of creating the widget
|
||||
// preview scaffold project directory as well.
|
||||
previewManifest.generate();
|
||||
|
||||
// The widget preview scaffold project directory should exist as well as the newly generated
|
||||
// preview manifest.
|
||||
expect(previewManifest.shouldGenerateProject(), false);
|
||||
|
||||
// Simulate changing the root project's pubspec.yaml and verify that we should regenerate
|
||||
// the widget preview scaffold's pubspec.yaml.
|
||||
rootProject.replacePubspec(
|
||||
rootProject.manifest.copyWith(logger: logger, models: <Uri>[Uri(host: 'Random')]),
|
||||
);
|
||||
expect(previewManifest.shouldRegeneratePubspec(), true);
|
||||
|
||||
// Update the manifest to include the hash for the updated pubspec.yaml and verify that we
|
||||
// no longer need to regenerate the pubspec.
|
||||
previewManifest.updatePubspecHash();
|
||||
expect(previewManifest.shouldRegeneratePubspec(), false);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user