flutter/packages/flutter_tools/lib/src/web/module_metadata.dart
Srujan Gaddam 8e2a6fc3fd
Implement hot reload using the DDC library bundle format (#162498)
https://github.com/dart-lang/webdev/issues/2516

- Updates restart/reload code to accept a resetCompiler boolean to
disambiguate between whether this is a full restart and whether to reset
the resident compiler.
- Adds code to call reloadSources in DWDS and handle the response
(including any errors).
- Adds code to invoke reassemble.
- Adds code to emit a script that DWDS can later consume that contains
the changed sources and their associated libraries. This is used to hot
reload. The bootstrapper puts this in the global window. DWDS should be
updated to accept it in the provider itself. See
https://github.com/dart-lang/webdev/issues/2584.
- Adds code to parse module metadata from the frontend server. This is
identical to the implementation in DWDS % addressing type-related lints.
- Adds tests that run the existing hot reload tests but with web. Some
modifications are mode, including waiting for Flutter runs to finish
executing, and skipping a test that's not possible on the web.

Needs DWDS 24.3.4 to be published first and used before we can land.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
2025-02-03 06:10:24 +00:00

204 lines
6.8 KiB
Dart

// 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.
// Taken from https://github.com/dart-lang/webdev/blob/616da45582e008efa114728927eabb498c71f1b7/dwds/lib/src/debugging/metadata/module_metadata.dart.
// Prefer to keep the implementations consistent.
/// Module metadata format version
///
/// Module reader always creates the current version but is able to read
/// metadata files with later versions as long as the changes are backward
/// compatible, i.e. only minor or patch versions have changed.
class ModuleMetadataVersion {
const ModuleMetadataVersion(this.majorVersion, this.minorVersion, this.patchVersion);
final int majorVersion;
final int minorVersion;
final int patchVersion;
/// Current metadata version
///
/// Version follows simple semantic versioning format 'major.minor.patch'
/// See https://semver.org
static const ModuleMetadataVersion current = ModuleMetadataVersion(2, 0, 0);
/// Previous version supported by the metadata reader
static const ModuleMetadataVersion previous = ModuleMetadataVersion(1, 0, 0);
/// Current metadata version created by the reader
String get version => '$majorVersion.$minorVersion.$patchVersion';
/// Is this metadata version compatible with the given version
///
/// The minor and patch version changes never remove any fields that current
/// version supports, so the reader can create current metadata version from
/// any file created with a later writer, as long as the major version does
/// not change.
bool isCompatibleWith(String version) {
final List<String> parts = version.split('.');
if (parts.length != 3) {
throw FormatException(
'Version: $version'
'does not follow simple semantic versioning format',
);
}
final int major = int.parse(parts[0]);
final int minor = int.parse(parts[1]);
final int patch = int.parse(parts[2]);
return major == majorVersion && minor >= minorVersion && patch >= patchVersion;
}
}
/// Library metadata
///
/// Represents library metadata used in the debugger,
/// supports reading from and writing to json.
class LibraryMetadata {
LibraryMetadata(this.name, this.importUri, this.partUris);
LibraryMetadata.fromJson(Map<String, Object?> json)
: name = _readRequiredField(json, nameField),
importUri = _readRequiredField(json, importUriField),
partUris = _readOptionalList(json, partUrisField) ?? <String>[];
static const String nameField = 'name';
static const String importUriField = 'importUri';
static const String partUrisField = 'partUris';
/// Library name as defined in pubspec.yaml
final String name;
/// Library importUri
///
/// Example package:path/path.dart
final String importUri;
/// All file uris from the library
///
/// Can be relative paths to the directory of the fileUri
final List<String> partUris;
Map<String, Object?> toJson() {
return <String, Object?>{
nameField: name,
importUriField: importUri,
partUrisField: <String>[...partUris],
};
}
}
/// Module metadata
///
/// Represents module metadata used in the debugger,
/// supports reading from and writing to json.
class ModuleMetadata {
ModuleMetadata(this.name, this.closureName, this.sourceMapUri, this.moduleUri, {String? ver}) {
version = ver ?? ModuleMetadataVersion.current.version;
}
ModuleMetadata.fromJson(Map<String, Object?> json)
: version = _readRequiredField(json, versionField),
name = _readRequiredField(json, nameField),
closureName = _readRequiredField(json, closureNameField),
sourceMapUri = _readRequiredField(json, sourceMapUriField),
moduleUri = _readRequiredField(json, moduleUriField) {
if (!ModuleMetadataVersion.current.isCompatibleWith(version) &&
!ModuleMetadataVersion.previous.isCompatibleWith(version)) {
throw Exception(
'Unsupported metadata version $version. '
'\n Supported versions: '
'\n ${ModuleMetadataVersion.current.version} '
'\n ${ModuleMetadataVersion.previous.version}',
);
}
for (final Map<String, Object?> l in _readRequiredList<Map<String, Object?>>(
json,
librariesField,
)) {
addLibrary(LibraryMetadata.fromJson(l));
}
}
static const String versionField = 'version';
static const String nameField = 'name';
static const String closureNameField = 'closureName';
static const String sourceMapUriField = 'sourceMapUri';
static const String moduleUriField = 'moduleUri';
static const String librariesField = 'libraries';
/// Metadata format version
late final String version;
/// Module name
///
/// Used as a name of the js module created by the compiler and
/// as key to store and load modules in the debugger and the browser
// TODO(srujzs): Remove once https://github.com/dart-lang/sdk/issues/59618 is
// resolved.
final String name;
/// Name of the function enclosing the module
///
/// Used by debugger to determine the top dart scope
final String closureName;
/// Source map uri
final String sourceMapUri;
/// Module uri
final String moduleUri;
final Map<String, LibraryMetadata> libraries = <String, LibraryMetadata>{};
/// Add [library] to metadata
///
/// Used for filling the metadata in the compiler or for reading from
/// stored metadata files.
void addLibrary(LibraryMetadata library) {
if (!libraries.containsKey(library.importUri)) {
libraries[library.importUri] = library;
} else {
throw Exception(
'Metadata creation error: '
'Cannot add library $library with uri ${library.importUri}: '
'another library "${libraries[library.importUri]}" is found '
'with the same uri',
);
}
}
Map<String, Object?> toJson() {
return <String, Object?>{
versionField: version,
nameField: name,
closureNameField: closureName,
sourceMapUriField: sourceMapUri,
moduleUriField: moduleUri,
librariesField: <Map<String, Object?>>[
for (final LibraryMetadata lib in libraries.values) lib.toJson(),
],
};
}
}
T _readRequiredField<T>(Map<String, Object?> json, String field) {
if (!json.containsKey(field)) {
throw FormatException('Required field $field is not set in $json');
}
return json[field]! as T;
}
T? _readOptionalField<T>(Map<String, Object?> json, String field) => json[field] as T?;
List<T> _readRequiredList<T>(Map<String, Object?> json, String field) {
final List<Object?> list = _readRequiredField<List<Object?>>(json, field);
return List.castFrom<Object?, T>(list);
}
List<T>? _readOptionalList<T>(Map<String, Object?> json, String field) {
final List<Object?>? list = _readOptionalField<List<Object?>>(json, field);
return list == null ? null : List.castFrom<Object?, T>(list);
}