| // Copyright (c) 2017, 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:async'; |
| import 'dart:convert'; |
| |
| import 'package:barback/barback.dart'; |
| import 'package:path/path.dart' as p; |
| |
| import 'errors.dart'; |
| import 'module.dart'; |
| import 'summaries.dart'; |
| |
| typedef FutureOr<String> ReadAsString(AssetId id); |
| |
| /// The name of the module config files. |
| const moduleConfigName = '.moduleConfig'; |
| |
| /// Reads and caches [Module]s, and allows you to get transitive dependencies. |
| class ModuleReader { |
| final ReadAsString assetReader; |
| |
| final _modulesByAssetId = <AssetId, Module>{}; |
| |
| final _modulesByModuleId = <ModuleId, Module>{}; |
| |
| final _moduleConfigFutures = <AssetId, Future<List<Module>>>{}; |
| |
| ModuleReader(this.assetReader); |
| |
| /// Returns a [Future<Module>] containing [id]. |
| /// |
| /// The module config is expected to live directly inside the top level |
| /// directory of the package containing [id]. |
| /// |
| /// For example: |
| /// |
| /// id -> myapp|test/stuff/thing.dart |
| /// config -> myapp|test/.moduleConfig |
| /// |
| /// Throws a [MissingModuleException] if it can't find a module for [id]. |
| Future<Module> moduleFor(AssetId id) async { |
| var parts = p.split(p.dirname(id.path)); |
| if (parts.isEmpty) { |
| throw new ArgumentError("Unexpected asset `$id` which isn't under a top " |
| "level directory of its package."); |
| } |
| var moduleConfigId = |
| new AssetId(id.package, p.join(parts.first, moduleConfigName)); |
| await readModules(moduleConfigId); |
| Module module; |
| if (id.extension == '.dart') { |
| module = _modulesByAssetId[id]; |
| } else { |
| var baseName = p.url.basename(id.path); |
| String moduleName; |
| if (baseName.endsWith('.js')) { |
| moduleName = p.withoutExtension(baseName); |
| } else if (baseName.endsWith('.js.map')) { |
| moduleName = baseName.substring(0, baseName.length - '.js.map'.length); |
| } else if (baseName.endsWith(unlinkedSummaryExtension)) { |
| moduleName = baseName.substring( |
| 0, baseName.length - unlinkedSummaryExtension.length); |
| } else if (baseName.endsWith(linkedSummaryExtension)) { |
| moduleName = baseName.substring( |
| 0, baseName.length - linkedSummaryExtension.length); |
| } |
| if (moduleName == null) { |
| throw new ArgumentError( |
| 'Can only get modules for `.js` or `.dart` files, but got `$id`.'); |
| } |
| var moduleId = new ModuleId(id.package, moduleName, parts.first); |
| module = _modulesByModuleId[moduleId]; |
| } |
| if (module == null) throw new MissingModuleException(id); |
| return module; |
| } |
| |
| /// Computes the transitive deps of [id] by reading all the modules for all |
| /// its dependencies recursively. |
| /// |
| /// Assumes that any dependencies' modules are either already loaded or exist |
| /// in the default module config file for their package. |
| Future<Set<ModuleId>> readTransitiveDeps(Module module) async { |
| var result = new Set<ModuleId>(); |
| Future updateDeps(Iterable<AssetId> assetDepIds) async { |
| for (var assetDepId in assetDepIds) { |
| var assetDepModule = await moduleFor(assetDepId); |
| if (assetDepModule == null) { |
| throw new StateError( |
| 'Unable to find module for asset `$assetDepId`. This indicates ' |
| 'that either the file doesn\'t exist or it is not imported by ' |
| 'any public entrypoints in its package (files not under `lib/src`' |
| '). Importing a file directly that lives under `lib/src` is not ' |
| 'supported by the dartdevc transformers.'); |
| } |
| if (!result.add(assetDepModule.id)) continue; |
| await updateDeps(assetDepModule.directDependencies); |
| } |
| } |
| |
| await updateDeps(module.directDependencies); |
| return result; |
| } |
| |
| /// Loads all [Module]s in [moduleConfigId] if they are not already loaded. |
| /// |
| /// Populates [_modules] and [_modulesByAssetId] for each loaded [Module]. |
| /// |
| /// Returns a [Future<List<Module>>] representing all modules contained in |
| /// [moduleConfigId]. |
| Future<List<Module>> readModules(AssetId moduleConfigId) { |
| return _moduleConfigFutures.putIfAbsent(moduleConfigId, () async { |
| var modules = <Module>[]; |
| var content = await assetReader(moduleConfigId); |
| var serializedModules = JSON.decode(content) as List<List<List<dynamic>>>; |
| for (var serializedModule in serializedModules) { |
| var module = new Module.fromJson(serializedModule); |
| modules.add(module); |
| _modulesByModuleId[module.id] = module; |
| for (var id in module.assetIds) { |
| if (_modulesByAssetId.containsKey(id)) { |
| throw new StateError('Assets can only exist in one module, but $id ' |
| 'was found in both ${_modulesByAssetId[id].id} and ' |
| '${module.id}'); |
| } |
| _modulesByAssetId[id] = module; |
| } |
| } |
| return modules; |
| }); |
| } |
| |
| /// Invalidates all [Module]s for [package]. |
| void invalidatePackage(String package) { |
| // `map` must be of type <AssetId|ModuleId, dynamic> |
| removePackage(Map map) { |
| var idsToRemove = map.keys.where((id) => id.package == package).toList(); |
| for (var id in idsToRemove) { |
| map.remove(id); |
| } |
| } |
| |
| removePackage(_moduleConfigFutures); |
| removePackage(_modulesByModuleId); |
| removePackage(_modulesByAssetId); |
| } |
| } |