blob: ec21b0742b8d0769ed1e3d78ac484e4244b2c34a [file] [log] [blame]
// 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);
}
}