[ Widget Preview ] Pass DTD URI as a constant in a generated file (#179821)

Passing the DTD URI via a Dart define results in a new
`<hash>.cache.dill.track.dill` being created on each run as the DTD URI
is never the same and the `cache.dill.track.dill` hash is partly based
on the set of Dart defines. These files are not cleaned up
automatically, so they could take up a significant amount of memory over
time.

This change adds an additional code generation step to the widget
previewer which populates
`widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart` with a
constant containing the DTD URI provided by the tool.

Fixes https://github.com/flutter/flutter/issues/179139
This commit is contained in:
Ben Konyi 2025-12-15 10:01:29 -05:00 committed by GitHub
parent a588c85d8d
commit a78aefa9bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 77 additions and 22 deletions

View File

@ -401,6 +401,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
await _dtdService.connect(dtdWsUri: existingDtdUri);
}
_previewCodeGenerator.populateDtdConnectionInfo(_dtdService.dtdUri!);
}
Future<int> runPreviewEnvironment({required FlutterProject widgetPreviewScaffoldProject}) async {
@ -463,12 +464,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
BuildMode.debug,
null,
treeShakeIcons: false,
// Provide the DTD connection information directly to the preview scaffold.
// This could, in theory, be provided via a follow up call to a service extension
// registered by the preview scaffold, but there's some uncertainty around how service
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
// connection.
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
packageConfig: PackageConfig.parseBytes(
widgetPreviewScaffoldProject.packageConfig.readAsBytesSync(),

View File

@ -52,6 +52,35 @@ class PreviewCodeGenerator {
static String getGeneratedPreviewFilePath(FileSystem fs) =>
fs.path.join('lib', 'src', 'generated_preview.dart');
static String getGeneratedDtdConnectionInfoFilePath(FileSystem fs) =>
fs.path.join('lib', 'src', 'dtd', 'dtd_connection_info.dart');
void populateDtdConnectionInfo(Uri dtdUri) {
final emitter = cb.DartEmitter.scoped(useNullSafetySyntax: true);
final lib = cb.Library(
(cb.LibraryBuilder b) => b
..ignoreForFile.add('implementation_imports')
..body.addAll(<cb.Spec>[
cb.Field((b) {
b
..name = 'kWidgetPreviewDtdUri'
..modifier = cb.FieldModifier.constant
..type = cb.refer('String')
..assignment = cb.literalString(dtdUri.toString()).code;
}),
]),
);
final File generatedDtdConnectionInfoFile = fs.file(
widgetPreviewScaffoldProject.directory.uri.resolve(getGeneratedDtdConnectionInfoFilePath(fs)),
);
generatedDtdConnectionInfoFile.writeAsStringSync(
// Format the generated file for readability, particularly during feature development.
// Note: we don't really care _how_ this is formatted, just that it's formatted, so we don't
// specify a language version.
DartFormatter(languageVersion: Version.none).format(lib.accept(emitter).toString()),
);
}
// TODO(bkonyi): update generated example now that we're computing constants
/// Generates code used by the widget preview scaffold based on the preview instances listed in
/// [previews].

View File

@ -361,6 +361,7 @@
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/split.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd/utils.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd/editor_service.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl",

View File

@ -0,0 +1,5 @@
// 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.
const String kWidgetPreviewDtdUri = '';

View File

@ -6,14 +6,12 @@ import 'dart:async';
import 'package:dtd/dtd.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart';
import 'package:widget_preview_scaffold/src/dtd/editor_service.dart';
import 'package:widget_preview_scaffold/src/dtd/utils.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
// WARNING: Keep these constants and services in sync with those defined in the widget preview
// scaffold's dtd_services.dart.
//
@ -38,9 +36,7 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect({Uri? dtdUri}) async {
final Uri dtdWsUri =
dtdUri ??
Uri.parse(const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar));
final Uri dtdWsUri = dtdUri ?? Uri.parse(kWidgetPreviewDtdUri);
dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
dtd.postEvent(

View File

@ -245,7 +245,12 @@ void main() {
onChangeDetected: (_) {},
onPubspecChangeDetected: (String path) {},
);
codeGenerator = PreviewCodeGenerator(widgetPreviewScaffoldProject: project, fs: fs);
codeGenerator = PreviewCodeGenerator(
widgetPreviewScaffoldProject: FlutterProject.fromDirectoryTest(
project.widgetPreviewScaffold,
),
fs: fs,
);
final pub = Pub.test(
fileSystem: fs,
logger: logger,
@ -267,15 +272,15 @@ void main() {
'correctly generates ${PreviewCodeGenerator.getGeneratedPreviewFilePath(fs)}',
() async {
// Check that the generated preview file doesn't exist yet.
final File generatedPreviewFile = project.directory.childFile(
final File generatedPreviewFile = project.widgetPreviewScaffold.childFile(
PreviewCodeGenerator.getGeneratedPreviewFilePath(fs),
);
expect(generatedPreviewFile, isNot(exists));
generatedPreviewFile.createSync(recursive: true);
final PreviewDependencyGraph details = await previewDetector.initialize();
// Populate the generated preview file.
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(details);
expect(generatedPreviewFile, exists);
const expectedGeneratedPreviewFileContents = '''
// ignore_for_file: implementation_imports
@ -385,7 +390,6 @@ List<_i1.WidgetPreview> previews() => [
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(
const <PreviewPath, LibraryPreviewNode>{},
);
expect(generatedPreviewFile, exists);
// The generated file should only contain:
// - An import of the widget preview library
@ -401,5 +405,29 @@ List<_i1.WidgetPreview> previews() => [];
expect(generatedPreviewFile.readAsStringSync(), emptyGeneratedPreviewFileContents);
},
);
testUsingContext(
'correctly generates ${PreviewCodeGenerator.getGeneratedDtdConnectionInfoFilePath(fs)}',
() async {
// Check that the generated preview file doesn't exist yet.
final File generatedDtdConnectionInfoFile = project.widgetPreviewScaffold.childFile(
PreviewCodeGenerator.getGeneratedDtdConnectionInfoFilePath(fs),
);
expect(generatedDtdConnectionInfoFile, isNot(exists));
generatedDtdConnectionInfoFile.createSync(recursive: true);
// Populate the DTD connection info.
final Uri dtdUri = Uri.parse('ws://localhost:1234');
codeGenerator.populateDtdConnectionInfo(dtdUri);
final expectedDtdConnectionInfo =
'''
// ignore_for_file: implementation_imports
const String kWidgetPreviewDtdUri = '$dtdUri';
''';
expect(generatedDtdConnectionInfoFile.readAsStringSync(), expectedDtdConnectionInfo);
},
);
});
}

View File

@ -0,0 +1,5 @@
// 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.
const String kWidgetPreviewDtdUri = '';

View File

@ -6,14 +6,12 @@ import 'dart:async';
import 'package:dtd/dtd.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart';
import 'package:widget_preview_scaffold/src/dtd/editor_service.dart';
import 'package:widget_preview_scaffold/src/dtd/utils.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
// WARNING: Keep these constants and services in sync with those defined in the widget preview
// scaffold's dtd_services.dart.
//
@ -38,9 +36,7 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect({Uri? dtdUri}) async {
final Uri dtdWsUri =
dtdUri ??
Uri.parse(const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar));
final Uri dtdWsUri = dtdUri ?? Uri.parse(kWidgetPreviewDtdUri);
dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
dtd.postEvent(