| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:async/async.dart'; |
| import 'package:dwds/src/debugging/metadata/module_metadata.dart'; |
| import 'package:dwds/src/readers/asset_reader.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:path/path.dart' as p; |
| |
| /// A provider of metadata in which data is collected through DDC outputs. |
| class MetadataProvider { |
| final AssetReader _assetReader; |
| final _logger = Logger('MetadataProvider'); |
| final String entrypoint; |
| final Set<String> _libraries = {}; |
| final Map<String, String> _scriptToModule = {}; |
| final Map<String, String> _moduleToSourceMap = {}; |
| final Map<String, String> _modulePathToModule = {}; |
| final Map<String, String> _moduleToModulePath = {}; |
| final Map<String, Set<String>> _moduleToLibraries = {}; |
| final Map<String, List<String>> _scripts = {}; |
| final _metadataMemoizer = AsyncMemoizer(); |
| |
| /// Implicitly imported libraries in any DDC component. |
| /// |
| /// Currently dart_sdk module does not come with the metadata. |
| /// To allow evaluation of expressions that use libraries and |
| /// types from the SDK (such as a dart Type object), add the |
| /// metadata for dart_sdk manually. |
| /// |
| /// TODO: Generate sdk module metadata to be consumed by debugger. |
| /// Issue: https://github.com/dart-lang/sdk/issues/45477 |
| List<String> get sdkLibraries => const [ |
| 'dart:_runtime', |
| 'dart:_debugger', |
| 'dart:_foreign_helper', |
| 'dart:_interceptors', |
| 'dart:_internal', |
| 'dart:_isolate_helper', |
| 'dart:_js_helper', |
| 'dart:_js_primitives', |
| 'dart:_metadata', |
| 'dart:_native_typed_data', |
| 'dart:_rti', |
| 'dart:async', |
| 'dart:collection', |
| 'dart:convert', |
| 'dart:core', |
| 'dart:developer', |
| 'dart:io', |
| 'dart:isolate', |
| 'dart:js', |
| 'dart:js_util', |
| 'dart:math', |
| 'dart:typed_data', |
| 'dart:indexed_db', |
| 'dart:html', |
| 'dart:html_common', |
| 'dart:svg', |
| 'dart:web_audio', |
| 'dart:web_gl', |
| 'dart:ui', |
| ]; |
| |
| MetadataProvider(this.entrypoint, this._assetReader); |
| |
| /// A list of all libraries in the Dart application. |
| /// |
| /// Example: |
| /// |
| /// [ |
| /// dart:web_gl, |
| /// dart:math, |
| /// org-dartlang-app:///web/main.dart |
| /// ] |
| /// |
| Future<List<String>> get libraries async { |
| await _initialize(); |
| return _libraries.toList(); |
| } |
| |
| /// A map of library uri to dart scripts. |
| /// |
| /// Example: |
| /// |
| /// { |
| /// org-dartlang-app:///web/main.dart : |
| /// { web/main.dart } |
| /// } |
| /// |
| Future<Map<String, List<String>>> get scripts async { |
| await _initialize(); |
| return _scripts; |
| } |
| |
| /// A map of script to containing module. |
| /// |
| /// Example: |
| /// |
| /// { |
| /// org-dartlang-app:///web/main.dart : |
| /// web/main |
| /// } |
| Future<Map<String, String>> get scriptToModule async { |
| await _initialize(); |
| return _scriptToModule; |
| } |
| |
| /// A map of module name to source map path. |
| /// |
| /// Example: |
| /// |
| /// { |
| /// org-dartlang-app:///web/main.dart : |
| /// web/main.ddc.js.map |
| /// } |
| Future<Map<String, String>> get moduleToSourceMap async { |
| await _initialize(); |
| return _moduleToSourceMap; |
| } |
| |
| /// A map of module path to module name. |
| /// |
| /// Example: |
| /// |
| /// { |
| /// web/main.ddc.js : |
| /// web/main |
| /// } |
| Future<Map<String, String>> get modulePathToModule async { |
| await _initialize(); |
| return _modulePathToModule; |
| } |
| |
| /// A map of module to module path. |
| /// |
| /// Example: |
| /// |
| /// { |
| /// web/main |
| /// web/main.ddc.js : |
| /// } |
| Future<Map<String, String>> get moduleToModulePath async { |
| await _initialize(); |
| return _moduleToModulePath; |
| } |
| |
| /// A list of module ids. |
| /// |
| /// Example: |
| /// |
| /// [ |
| /// web/main, |
| /// web/foo/bar |
| /// ] |
| Future<List<String>> get modules async { |
| await _initialize(); |
| return _moduleToModulePath.keys.toList(); |
| } |
| |
| /// Compute metadata information after reading the metadata contents and |
| /// return a map from module names to their [ModuleMetadata]. |
| Future<Map<String, ModuleMetadata>> _processMetadata() async { |
| final modules = <String, ModuleMetadata>{}; |
| // The merged metadata resides next to the entrypoint. |
| // Assume that <name>.bootstrap.js has <name>.ddc_merged_metadata |
| if (entrypoint.endsWith('.bootstrap.js')) { |
| _logger.info('Loading debug metadata...'); |
| final serverPath = entrypoint.replaceAll( |
| '.bootstrap.js', |
| '.ddc_merged_metadata', |
| ); |
| final merged = await _assetReader.metadataContents(serverPath); |
| if (merged != null) { |
| for (final contents in merged.split('\n')) { |
| try { |
| if (contents.isEmpty || |
| contents.startsWith('// intentionally empty:')) { |
| continue; |
| } |
| final moduleJson = json.decode(contents); |
| final metadata = ModuleMetadata.fromJson( |
| moduleJson as Map<String, dynamic>, |
| ); |
| final moduleName = metadata.name; |
| modules[moduleName] = metadata; |
| _logger.fine('Loaded debug metadata for module: $moduleName'); |
| } catch (e) { |
| _logger.warning('Failed to read metadata: $e'); |
| rethrow; |
| } |
| } |
| } |
| } |
| return modules; |
| } |
| |
| /// Process all metadata, including SDK metadata, and compute caches once. |
| Future<void> _initialize() async { |
| await _metadataMemoizer.runOnce(() async { |
| final metadata = await _processMetadata(); |
| _addSdkMetadata(); |
| metadata.values.forEach(_addMetadata); |
| }); |
| } |
| |
| /// Given a map of hot reloaded modules mapped to their respective libraries, |
| /// determines deleted and invalidated libraries and modules, invalidates them |
| /// in any caches, and recomputes the necessary information. |
| /// |
| /// Returns a [ModifiedModuleReport] that can be used to invalidate other |
| /// caches after a hot reload. |
| Future<ModifiedModuleReport> reinitializeAfterHotReload( |
| Map<String, List> reloadedModulesToLibraries, |
| ) async { |
| final modules = await _processMetadata(); |
| final invalidatedLibraries = <String>{}; |
| void invalidateLibrary(String libraryImportUri) { |
| invalidatedLibraries.add(libraryImportUri); |
| _libraries.remove(libraryImportUri); |
| _scriptToModule.remove(libraryImportUri); |
| _scripts[libraryImportUri]?.forEach(_scriptToModule.remove); |
| _scripts.remove(libraryImportUri); |
| } |
| |
| final deletedModules = <String>{}; |
| for (final module in _moduleToLibraries.keys) { |
| final deletedModule = !modules.containsKey(module); |
| final invalidatedModule = reloadedModulesToLibraries.containsKey(module); |
| assert(!(deletedModule && invalidatedModule)); |
| // If the module was either deleted or reloaded, invalidate all previous |
| // information both about the module and its libraries. |
| if (deletedModule || invalidatedModule) { |
| _modulePathToModule.remove(module); |
| _moduleToLibraries[module]?.forEach(invalidateLibrary); |
| _moduleToModulePath.remove(module); |
| _moduleToSourceMap.remove(module); |
| } |
| if (deletedModule) deletedModules.add(module); |
| } |
| final reloadedModules = <String>{}; |
| final reloadedLibraries = <String>{}; |
| for (final module in reloadedModulesToLibraries.keys) { |
| reloadedModules.add(module); |
| reloadedLibraries.addAll( |
| reloadedModulesToLibraries[module]!.cast<String>(), |
| ); |
| _addMetadata(modules[module]!); |
| } |
| // The libraries that are removed from the program are those that we |
| // invalidated but were never added again. |
| final deletedLibraries = |
| invalidatedLibraries |
| .where((library) => !_libraries.contains(library)) |
| .toSet(); |
| return ModifiedModuleReport( |
| deletedModules: deletedModules, |
| deletedLibraries: deletedLibraries, |
| reloadedModules: reloadedModules, |
| reloadedLibraries: reloadedLibraries, |
| ); |
| } |
| |
| void _addMetadata(ModuleMetadata metadata) { |
| final modulePath = stripLeadingSlashes(metadata.moduleUri); |
| final sourceMapPath = stripLeadingSlashes(metadata.sourceMapUri); |
| final moduleName = metadata.name; |
| |
| _moduleToSourceMap[moduleName] = sourceMapPath; |
| _modulePathToModule[modulePath] = moduleName; |
| _moduleToModulePath[moduleName] = modulePath; |
| |
| final moduleLibraries = <String>{}; |
| for (final library in metadata.libraries.values) { |
| if (library.importUri.startsWith('file:/')) { |
| throw AbsoluteImportUriException(library.importUri); |
| } |
| moduleLibraries.add(library.importUri); |
| _libraries.add(library.importUri); |
| _scripts[library.importUri] = []; |
| |
| _scriptToModule[library.importUri] = moduleName; |
| for (final path in library.partUris) { |
| // Parts in metadata are relative to the library Uri directory. |
| final partPath = p.url.join(p.dirname(library.importUri), path); |
| _scripts[library.importUri]!.add(partPath); |
| _scriptToModule[partPath] = moduleName; |
| } |
| } |
| _moduleToLibraries[moduleName] = moduleLibraries; |
| } |
| |
| void _addSdkMetadata() { |
| final moduleName = 'dart_sdk'; |
| |
| for (final lib in sdkLibraries) { |
| _libraries.add(lib); |
| _scripts[lib] = []; |
| // TODO(srujzs): It feels weird that we add this mapping to only this map |
| // and not any of the other module maps. We should maybe handle this |
| // differently. This will become relevant if we ever support hot reload |
| // for the Dart SDK. |
| _scriptToModule[lib] = moduleName; |
| } |
| } |
| } |
| |
| class AbsoluteImportUriException implements Exception { |
| final String importUri; |
| AbsoluteImportUriException(this.importUri); |
| |
| @override |
| String toString() => "AbsoluteImportUriError: '$importUri'"; |
| } |
| |
| /// Computed after a hot reload using |
| /// [MetadataProvider.reinitializeAfterHotReload], represents the modules and |
| /// libraries in the program that were deleted, reloaded, and therefore, |
| /// modified. |
| /// |
| /// Used to recompute caches throughout DWDS. |
| class ModifiedModuleReport { |
| /// Module names that are no longer in the program. |
| final Set<String> deletedModules; |
| |
| /// Library uris that are no longer in the program. |
| final Set<String> deletedLibraries; |
| |
| /// Module names that were loaded during the hot reload. |
| final Set<String> reloadedModules; |
| |
| /// Library uris that were loaded during the hot reload. |
| final Set<String> reloadedLibraries; |
| |
| /// Module names that were either removed or modified, including additions. |
| final Set<String> modifiedModules; |
| |
| /// Library uris that were either removed or modified, including additions. |
| final Set<String> modifiedLibraries; |
| ModifiedModuleReport({ |
| required this.deletedModules, |
| required this.deletedLibraries, |
| required this.reloadedModules, |
| required this.reloadedLibraries, |
| }) : modifiedModules = deletedModules.union(reloadedModules), |
| modifiedLibraries = deletedLibraries.union(reloadedLibraries); |
| } |