| // 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; |
| bool _soundNullSafety; |
| final List<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, 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) |
| : _soundNullSafety = false; |
| |
| /// A sound null safety mode for the whole app. |
| /// |
| /// All libraries have to agree on null safety mode. |
| Future<bool> get soundNullSafety async { |
| await _initialize(); |
| return _soundNullSafety; |
| } |
| |
| /// 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; |
| } |
| |
| /// 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(); |
| } |
| |
| Future<void> _initialize() async { |
| await _metadataMemoizer.runOnce(() async { |
| var hasSoundNullSafety = true; |
| var hasUnsoundNullSafety = true; |
| // 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) { |
| _addSdkMetadata(); |
| for (var 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>); |
| _addMetadata(metadata); |
| hasUnsoundNullSafety &= !metadata.soundNullSafety; |
| hasSoundNullSafety &= metadata.soundNullSafety; |
| _logger |
| .fine('Loaded debug metadata for module: ${metadata.name}'); |
| } catch (e) { |
| _logger.warning('Failed to read metadata: $e'); |
| rethrow; |
| } |
| } |
| if (!hasSoundNullSafety && !hasUnsoundNullSafety) { |
| throw Exception('Metadata contains modules with mixed null safety'); |
| } |
| _soundNullSafety = hasSoundNullSafety; |
| } |
| _logger.info('Loaded debug metadata ' |
| '(${_soundNullSafety ? "sound" : "weak"} null safety)'); |
| } |
| }); |
| } |
| |
| void _addMetadata(ModuleMetadata metadata) { |
| final modulePath = stripLeadingSlashes(metadata.moduleUri); |
| final sourceMapPath = stripLeadingSlashes(metadata.sourceMapUri); |
| |
| _moduleToSourceMap[metadata.name] = sourceMapPath; |
| _modulePathToModule[modulePath] = metadata.name; |
| _moduleToModulePath[metadata.name] = modulePath; |
| |
| for (var library in metadata.libraries.values) { |
| if (library.importUri.startsWith('file:/')) { |
| throw AbsoluteImportUriException(library.importUri); |
| } |
| _libraries.add(library.importUri); |
| _scripts[library.importUri] = []; |
| |
| _scriptToModule[library.importUri] = metadata.name; |
| for (var 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] = metadata.name; |
| } |
| } |
| } |
| |
| void _addSdkMetadata() { |
| final moduleName = 'dart_sdk'; |
| |
| for (var lib in sdkLibraries) { |
| _libraries.add(lib); |
| _scripts[lib] = []; |
| _scriptToModule[lib] = moduleName; |
| } |
| } |
| } |
| |
| class AbsoluteImportUriException implements Exception { |
| final String importUri; |
| AbsoluteImportUriException(this.importUri); |
| |
| @override |
| String toString() => "AbsoluteImportUriError: '$importUri'"; |
| } |