| // 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:async/async.dart'; |
| import 'package:barback/barback.dart'; |
| import 'package:cli_util/cli_util.dart' as cli_util; |
| import 'package:path/path.dart' as p; |
| |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../package_graph.dart'; |
| import 'dartdevc.dart'; |
| import 'module.dart'; |
| import 'module_computer.dart'; |
| import 'module_reader.dart'; |
| import 'scratch_space.dart'; |
| import 'summaries.dart'; |
| import 'workers.dart'; |
| |
| /// Handles running dartdevc on top of a [Barback] instance. |
| /// |
| /// You must call [invalidatePackage] any time a package is updated, since |
| /// barback doesn't provide a mechanism to tell you which files have changed. |
| class DartDevcEnvironment { |
| final _AssetCache _assetCache; |
| final Barback _barback; |
| final Map<String, String> _environmentConstants; |
| final BarbackMode _mode; |
| ModuleReader _moduleReader; |
| final PackageGraph _packageGraph; |
| ScratchSpace _scratchSpace; |
| |
| static final _sdkResources = <String, String>{ |
| 'dart_sdk.js': 'lib/dev_compiler/amd/dart_sdk.js', |
| 'require.js': 'lib/dev_compiler/amd/require.js', |
| 'dart_stack_trace_mapper.js': |
| 'lib/dev_compiler/web/dart_stack_trace_mapper.js', |
| 'ddc_web_compiler.js': 'lib/dev_compiler/web/ddc_web_compiler.js', |
| }; |
| |
| DartDevcEnvironment( |
| this._barback, this._mode, this._environmentConstants, this._packageGraph) |
| : _assetCache = new _AssetCache(_packageGraph) { |
| _moduleReader = new ModuleReader(_readModule); |
| _scratchSpace = new ScratchSpace(_getAsset); |
| } |
| |
| /// Deletes the [_scratchSpace] and shuts down the workers. |
| Future cleanUp() { |
| return Future.wait([ |
| _scratchSpace.delete(), |
| // These should get terminated automatically when this process exits, but |
| // we end them explicitly just to be safe. |
| analyzerDriver.terminateWorkers(), |
| dartdevcDriver.terminateWorkers() |
| ]); |
| } |
| |
| /// Builds all dartdevc files required for all app entrypoints in |
| /// [inputAssets]. |
| /// |
| /// Returns only the `.js` files which are required to load the apps. |
| Future<AssetSet> doFullBuild(AssetSet inputAssets, |
| {logError(String message)}) async { |
| try { |
| var modulesToBuild = new Set<ModuleId>(); |
| var jsAssets = new AssetSet(); |
| // All the dirs that we found app entrypoints under, we need to copy the |
| // static dartdevc resources into each of these directories as well. |
| var appDirs = new Set<String>(); |
| for (var asset in inputAssets) { |
| if (asset.id.package != _packageGraph.entrypoint.root.name) continue; |
| if (asset.id.extension != '.dart') continue; |
| // We only care about real entrypoint modules, we collect those and all |
| // their transitive deps. |
| if (!await isAppEntryPoint(asset.id, _barback.getAssetById)) continue; |
| appDirs.add(p.url.dirname(asset.id.path)); |
| // Build the entrypoint JS files, and collect the set of transitive |
| // modules that are required (will be built later). |
| var futureAssets = _buildAsset(asset.id.addExtension('.js')); |
| var assets = await Future.wait(futureAssets.values); |
| jsAssets.addAll(assets.where((asset) => asset != null)); |
| var module = await _moduleReader.moduleFor(asset.id); |
| modulesToBuild.add(module.id); |
| modulesToBuild.addAll(await _moduleReader.readTransitiveDeps(module)); |
| } |
| |
| // Build all required modules for the apps that were discovered. |
| var allFutureAssets = <Future<Asset>>[]; |
| for (var module in modulesToBuild) { |
| allFutureAssets.addAll(_buildAsset(module.jsId).values); |
| } |
| // Copy all JS resources for each of the app dirs that were discovered. |
| for (var dir in appDirs) { |
| for (var name in _sdkResources.keys) { |
| allFutureAssets.add(_buildJsResource(new AssetId( |
| _packageGraph.entrypoint.root.name, p.url.join(dir, name)))); |
| } |
| } |
| var assets = await Future.wait(allFutureAssets); |
| jsAssets.addAll(assets.where((asset) => asset != null)); |
| |
| return jsAssets; |
| } catch (e) { |
| logError(e); |
| return new AssetSet(); |
| } |
| } |
| |
| /// Attempt to get an [Asset] by [id], completes with an |
| /// [AssetNotFoundException] if the asset couldn't be built. |
| Future<Asset> getAssetById(AssetId id) async { |
| if (!_assetCache.containsKey(id)) { |
| if (_isEntrypointId(id)) { |
| var dartId = _entrypointDartId(id); |
| if (dartId != null && |
| await isAppEntryPoint(dartId, _barback.getAssetById)) { |
| _buildAsset(id); |
| } |
| } else { |
| _buildAsset(id); |
| } |
| } |
| if (!_assetCache.containsKey(id)) throw new AssetNotFoundException(id); |
| return _assetCache[id]; |
| } |
| |
| /// Invalidates [package] and all packages that depend on [package]. |
| void invalidatePackage(String package) { |
| _assetCache.invalidatePackage(package); |
| _moduleReader.invalidatePackage(package); |
| _scratchSpace.deletePackageFiles(package, |
| isRootPackage: package == _packageGraph.entrypoint.root.name); |
| } |
| |
| /// Handles building all assets that we know how to build. |
| /// |
| /// Completes with an [AssetNotFoundException] if the asset couldn't be built. |
| Map<AssetId, Future<Asset>> _buildAsset(AssetId id) { |
| if (_assetCache.containsKey(id)) return {id: _assetCache[id]}; |
| Map<AssetId, Future<Asset>> assets; |
| if (id.path.endsWith(unlinkedSummaryExtension)) { |
| assets = {id: createUnlinkedSummary(id, _moduleReader, _scratchSpace)}; |
| } else if (id.path.endsWith(linkedSummaryExtension)) { |
| assets = {id: createLinkedSummary(id, _moduleReader, _scratchSpace)}; |
| } else if (_isEntrypointId(id)) { |
| var dartId = _entrypointDartId(id); |
| if (dartId != null) { |
| assets = bootstrapDartDevcEntrypoint(dartId, _mode, _moduleReader); |
| } |
| } else if (_hasJsResource(id)) { |
| assets = {id: _buildJsResource(id)}; |
| } else if (id.extension == '.map' && |
| _hasJsResource(id.changeExtension(''))) { |
| // None of these resources have sourcemaps. |
| assets = {id: new Future.error(new AssetNotFoundException(id))}; |
| } else if (id.path.endsWith('.js') || id.path.endsWith('.js.map')) { |
| var jsId = id.extension == '.map' ? id.changeExtension('') : id; |
| assets = createDartdevcModule( |
| jsId, _moduleReader, _scratchSpace, _environmentConstants, _mode); |
| // Pre-emptively start building all transitive JS deps under the |
| // assumption they will be needed in the near future. |
| () async { |
| try { |
| var module = await _moduleReader.moduleFor(jsId); |
| var deps = await _moduleReader.readTransitiveDeps(module); |
| await Future |
| .wait(deps.map((moduleId) => getAssetById(moduleId.jsId))); |
| } catch (_) { |
| // Errors will be returned later when requests are made. |
| } |
| }(); |
| } else if (id.path.endsWith(moduleConfigName)) { |
| assets = {id: _buildModuleConfig(id)}; |
| } else if (id.extension == '.errors') { |
| assets = {id: _cachedAsset(id)}; |
| } |
| assets ??= <AssetId, Future<Asset>>{}; |
| |
| for (var assetId in assets.keys) { |
| _assetCache[assetId] = assets[assetId]; |
| } |
| return assets; |
| } |
| |
| /// Builds a module config asset at [id]. |
| Future<Asset> _buildModuleConfig(AssetId id) async { |
| assert(id.path.endsWith(moduleConfigName)); |
| var moduleDir = topLevelDir(id.path); |
| var allAssets = await _barback.getAllAssets(); |
| var moduleAssets = allAssets.where((asset) => |
| asset.id.package == id.package && |
| asset.id.extension == '.dart' && |
| topLevelDir(asset.id.path) == moduleDir); |
| if (moduleAssets.isEmpty) throw new AssetNotFoundException(id); |
| var moduleMode = |
| moduleDir == 'lib' ? ModuleMode.public : ModuleMode.private; |
| var modules = await computeModules(moduleMode, moduleAssets); |
| var encoded = JSON.encode(modules); |
| return new Asset.fromString(id, encoded); |
| } |
| |
| /// Returns a cached asset for [id] if one exists, otherwise throws an |
| /// [AssetNotFoundException]. |
| Future<Asset> _cachedAsset(AssetId id) async { |
| if (_assetCache.containsKey(id)) { |
| return _assetCache[id]; |
| } |
| throw new AssetNotFoundException(id); |
| } |
| |
| /// Whether [_sdkResources] has an asset matching [id]. |
| bool _hasJsResource(AssetId id) => |
| _sdkResources.containsKey(p.url.basename(id.path)); |
| |
| /// Builds [_sdkResources] assets by copying them from the SDK. |
| Future<Asset> _buildJsResource(AssetId id) async { |
| var sdk = cli_util.getSdkDir(); |
| var basename = p.url.basename(id.path); |
| var resourcePath = _sdkResources[basename]; |
| if (resourcePath == null) return null; |
| return new Asset.fromPath(id, p.url.join(sdk.path, resourcePath)); |
| } |
| |
| /// Whether or not this looks like a request for an entrypoint or bootstrap |
| /// file. |
| bool _isEntrypointId(AssetId id) => |
| id.path.endsWith('.bootstrap.js') || |
| id.path.endsWith('.bootstrap.js.map') || |
| id.path.endsWith('.dart.js') || |
| id.path.endsWith('.dart.js.map'); |
| |
| /// Helper to read a module config file, used by [_moduleReader]. |
| /// |
| /// Skips barback and reads directly from [this] since we create all these |
| /// files. |
| Future<String> _readModule(AssetId moduleConfigId) async { |
| var asset = await getAssetById(moduleConfigId); |
| return asset.readAsString(); |
| } |
| |
| /// Gets an [Asset] by [id] asynchronously. |
| /// |
| /// All `.dart` files are read from [_barback], and all other files are read |
| /// from [this]. This is because the only files we care about from barback are |
| /// `.dart` files. |
| Future<Asset> _getAsset(AssetId id) async { |
| var asset = id.extension == '.dart' |
| ? await _barback.getAssetById(id) |
| : await getAssetById(id); |
| if (asset == null) throw new AssetNotFoundException(id); |
| return asset; |
| } |
| } |
| |
| /// Gives the dart entrypoint [AssetId] for a bootstrap JS [id]. |
| AssetId _entrypointDartId(AssetId id) { |
| if (id.extension == '.map') id = id.changeExtension(''); |
| assert(id.path.endsWith('.bootstrap.js') || id.path.endsWith('.dart.js')); |
| // Skip entrypoints under lib. |
| if (topLevelDir(id.path) == 'lib') return null; |
| |
| // Remove the `.js` extension. |
| var dartId = id.changeExtension(''); |
| // Conditionally remove the `.bootstrap` extension. |
| if (dartId.extension == '.bootstrap') dartId = dartId.changeExtension(''); |
| assert(dartId.extension == '.dart'); |
| return dartId; |
| } |
| |
| /// Manages a set of cached future [Asset]s. |
| class _AssetCache { |
| /// [Asset]s are indexed first by package and then path, this allows us to |
| /// invalidate whole packages efficiently. |
| final _assets = <String, Map<String, Future<Result<Asset>>>>{}; |
| |
| final PackageGraph _packageGraph; |
| |
| _AssetCache(this._packageGraph); |
| |
| Future<Asset> operator [](AssetId id) { |
| var futureResult = _getResult(id); |
| if (futureResult == null) return null; |
| return Result.release(futureResult); |
| } |
| |
| void operator []=(AssetId id, Future<Asset> asset) { |
| var packageCache = _assets.putIfAbsent( |
| id.package, () => <String, Future<Result<Asset>>>{}); |
| packageCache[id.path] = Result.capture(asset.catchError((e, s) { |
| // Log the error eagerly, and only once. |
| log.error(null, log.red(e), s); |
| // Create an asset that contains the errors, the app may use this to get |
| // information about any failed request. |
| var errorAssetId = id.addExtension('.errors'); |
| this[errorAssetId] = |
| new Future.value(new Asset.fromString(errorAssetId, '$e')); |
| // Convert the error into an `AssetNotFoundException` which the server |
| // knows how to handle. |
| throw new AssetNotFoundException(id); |
| }, test: (e) => e is! AssetNotFoundException)); |
| } |
| |
| bool containsKey(AssetId id) => _getResult(id) != null; |
| |
| /// Invalidates [package] and all packages that depend on [package]. |
| void invalidatePackage(String packageNameToInvalidate) { |
| _assets.remove(packageNameToInvalidate); |
| // Also invalidate any package with a transitive dep on the invalidated |
| // package. |
| var packageToInvalidate = _packageGraph.packages[packageNameToInvalidate]; |
| for (var packageName in _packageGraph.packages.keys) { |
| if (_packageGraph |
| .transitiveDependencies(packageName) |
| .contains(packageToInvalidate)) { |
| _assets.remove(packageName); |
| } |
| } |
| } |
| |
| /// Reads a [Result] from the actual underlying caches, or returns `null`. |
| Future<Result> _getResult(AssetId id) { |
| var packageCache = _assets[id.package]; |
| if (packageCache == null) return null; |
| var futureResult = packageCache[id.path]; |
| if (futureResult == null) return null; |
| return futureResult; |
| } |
| } |