| // 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' show JsonEncoder; |
| import 'dart:io'; |
| |
| import 'package:analyzer/analyzer.dart'; |
| import 'package:barback/barback.dart'; |
| import 'package:bazel_worker/bazel_worker.dart'; |
| import 'package:cli_util/cli_util.dart' as cli_util; |
| import 'package:path/path.dart' as p; |
| |
| import '../dart.dart'; |
| import '../io.dart'; |
| import 'errors.dart'; |
| import 'module_reader.dart'; |
| import 'scratch_space.dart'; |
| import 'summaries.dart'; |
| import 'workers.dart'; |
| |
| /// JavaScript snippet to determine the directory a script was run from. |
| final _currentDirectoryScript = r''' |
| var _currentDirectory = (function () { |
| var _url; |
| var lines = new Error().stack.split('\n'); |
| function lookupUrl() { |
| if (lines.length > 2) { |
| var match = lines[1].match(/^\s+at (.+):\d+:\d+$/); |
| // Chrome. |
| if (match) return match[1]; |
| // Chrome nested eval case. |
| match = lines[1].match(/^\s+at eval [(](.+):\d+:\d+[)]$/); |
| if (match) return match[1]; |
| // Edge. |
| match = lines[1].match(/^\s+at.+\((.+):\d+:\d+\)$/); |
| if (match) return match[1]; |
| // Firefox. |
| match = lines[0].match(/[<][@](.+):\d+:\d+$/) |
| if (match) return match[1]; |
| } |
| // Safari. |
| return lines[0].match(/(.+):\d+:\d+$/)[1]; |
| } |
| _url = lookupUrl(); |
| var lastSlash = _url.lastIndexOf('/'); |
| if (lastSlash == -1) return _url; |
| var currentDirectory = _url.substring(0, lastSlash + 1); |
| return currentDirectory; |
| })(); |
| '''; |
| |
| /// Returns whether or not [dartId] is an app entrypoint (basically, whether or |
| /// not it has a `main` function). |
| Future<bool> isAppEntryPoint( |
| AssetId dartId, Future<Asset> getAsset(AssetId id)) async { |
| assert(dartId.extension == '.dart'); |
| var dartAsset = await getAsset(dartId); |
| // Skip reporting errors here, dartdevc will report them later with nicer |
| // formatting. |
| var parsed = parseCompilationUnit(await dartAsset.readAsString(), |
| suppressErrors: true); |
| return isEntrypoint(parsed); |
| } |
| |
| /// Bootstraps the JS module for the entrypoint dart file [dartEntrypointId] |
| /// with two additional JS files: |
| /// |
| /// * A `$dartEntrypointId.js` file which is the main entrypoint for the app. It |
| /// injects a script tag whose src is `require.js` and whose `data-main` |
| /// attribute points at a `$dartEntrypointId.bootstrap.js` file. |
| /// * A `$dartEntrypointId.bootstrap.js` file which invokes the top level `main` |
| /// function from the entrypoint module, after performing some necessary SDK |
| /// setup. |
| /// |
| /// In debug mode an empty sourcemap will be output for the entrypoint JS file |
| /// to satisfy the test package runner (there is no original dart file to map it |
| /// back to though). |
| /// |
| /// Synchronously returns a `Map<AssetId, Future<Asset>>` so that you can know |
| /// immediately what assets will be output. |
| Map<AssetId, Future<Asset>> bootstrapDartDevcEntrypoint( |
| AssetId dartEntrypointId, BarbackMode mode, ModuleReader moduleReader) { |
| var bootstrapId = dartEntrypointId.addExtension('.bootstrap.js'); |
| var jsEntrypointId = dartEntrypointId.addExtension('.js'); |
| var jsMapEntrypointId = jsEntrypointId.addExtension('.map'); |
| |
| var outputCompleters = <AssetId, Completer<Asset>>{ |
| bootstrapId: new Completer(), |
| jsEntrypointId: new Completer(), |
| }; |
| var isDebug = mode == BarbackMode.DEBUG; |
| |
| if (isDebug) { |
| outputCompleters[jsMapEntrypointId] = new Completer<Asset>(); |
| } |
| |
| return _ensureComplete(outputCompleters, () async { |
| var module = await moduleReader.moduleFor(dartEntrypointId); |
| |
| // The path to the entrypoint JS module as it should appear in the call to |
| // `require` in the bootstrap file. |
| var moduleDir = topLevelDir(dartEntrypointId.path); |
| var appModulePath = p.url.relative(p.url.join(moduleDir, module.id.name), |
| from: p.url.dirname(dartEntrypointId.path)); |
| |
| // The name of the entrypoint dart library within the entrypoint JS module. |
| // |
| // This is used to invoke `main()` from within the bootstrap script. |
| // |
| // TODO(jakemac53): Sane module name creation, this only works in the most |
| // basic of cases. |
| // |
| // See https://github.com/dart-lang/sdk/issues/27262 for the root issue |
| // which will allow us to not rely on the naming schemes that dartdevc uses |
| // internally, but instead specify our own. |
| var appModuleScope = p.url |
| .split(p.url.withoutExtension( |
| p.url.relative(dartEntrypointId.path, from: moduleDir))) |
| .join("__") |
| .replaceAll('.', '\$46'); |
| |
| // Map from module name to module path. |
| // Modules outside of the `packages` directory have different module path |
| // and module names. |
| var modulePaths = {appModulePath: appModulePath, 'dart_sdk': 'dart_sdk'}; |
| var transitiveDeps = await moduleReader.readTransitiveDeps(module); |
| for (var dep in transitiveDeps) { |
| if (dep.dir != 'lib') { |
| var relativePath = p.url.relative(p.url.join(moduleDir, dep.name), |
| from: p.url.dirname(bootstrapId.path)); |
| var jsModuleName = '${dep.dir}/${dep.name}'; |
| |
| modulePaths[jsModuleName] = relativePath; |
| } else { |
| var jsModuleName = 'packages/${dep.package}/${dep.name}'; |
| var actualModulePath = p.url.relative( |
| p.url.join(moduleDir, jsModuleName), |
| from: p.url.dirname(bootstrapId.path)); |
| modulePaths[jsModuleName] = actualModulePath; |
| } |
| } |
| var bootstrapContent = new StringBuffer('(function() {\n'); |
| if (isDebug) { |
| bootstrapContent.write(''' |
| $_currentDirectoryScript |
| let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)}; |
| |
| if(!window.\$dartLoader) { |
| window.\$dartLoader = { |
| moduleIdToUrl: new Map(), |
| urlToModuleId: new Map(), |
| rootDirectories: new Set(), |
| }; |
| } |
| |
| let customModulePaths = {}; |
| window.\$dartLoader.rootDirectories.add(_currentDirectory); |
| for (let moduleName of Object.getOwnPropertyNames(modulePaths)) { |
| let modulePath = modulePaths[moduleName]; |
| if (modulePath != moduleName) { |
| customModulePaths[moduleName] = modulePath; |
| } |
| var src = _currentDirectory + modulePath + '.js'; |
| if (window.\$dartLoader.moduleIdToUrl.has(moduleName)) { |
| continue; |
| } |
| \$dartLoader.moduleIdToUrl.set(moduleName, src); |
| \$dartLoader.urlToModuleId.set(src, moduleName); |
| } |
| '''); |
| } else { |
| var customModulePaths = <String, String>{}; |
| modulePaths.forEach((name, path) { |
| if (name != path) customModulePaths[name] = path; |
| }); |
| var json = const JsonEncoder.withIndent(" ").convert(customModulePaths); |
| bootstrapContent.write('let customModulePaths = ${json};\n'); |
| } |
| |
| bootstrapContent.write(''' |
| // Whenever we fail to load a JS module, try to request the corresponding |
| // `.errors` file, and log it to the console. |
| (function() { |
| var oldOnError = requirejs.onError; |
| requirejs.onError = function(e) { |
| if (e.originalError && e.originalError.srcElement) { |
| var xhr = new XMLHttpRequest(); |
| xhr.onreadystatechange = function() { |
| if (this.readyState == 4 && this.status == 200) { |
| console.error(this.responseText); |
| } |
| }; |
| xhr.open("GET", e.originalError.srcElement.src + ".errors", true); |
| xhr.send(); |
| } |
| // Also handle errors the normal way. |
| if (oldOnError) oldOnError(e); |
| }; |
| }()); |
| |
| require.config({ |
| waitSeconds: 30, |
| paths: customModulePaths |
| }); |
| |
| require(["$appModulePath", "dart_sdk"], function(app, dart_sdk) { |
| dart_sdk._isolate_helper.startRootIsolate(() => {}, []); |
| '''); |
| |
| if (isDebug) { |
| bootstrapContent.write(''' |
| dart_sdk._debugger.registerDevtoolsFormatter(); |
| |
| if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) { |
| window.\$dartStackTraceUtility.ready = true; |
| let dart = dart_sdk.dart; |
| window.\$dartStackTraceUtility.setSourceMapProvider( |
| function(url) { |
| var module = window.\$dartLoader.urlToModuleId.get(url); |
| if (!module) return null; |
| return dart.getSourceMap(module); |
| }); |
| } |
| window.postMessage({ type: "DDC_STATE_CHANGE", state: "start" }, "*"); |
| '''); |
| } |
| |
| bootstrapContent.write(''' |
| app.$appModuleScope.main(); |
| }); |
| })(); |
| '''); |
| outputCompleters[bootstrapId].complete( |
| new Asset.fromString(bootstrapId, bootstrapContent.toString())); |
| |
| var bootstrapModuleName = p.withoutExtension( |
| p.relative(bootstrapId.path, from: p.dirname(dartEntrypointId.path))); |
| var entrypointJsContent = new StringBuffer(''' |
| var el; |
| '''); |
| if (isDebug) { |
| entrypointJsContent.write(''' |
| el = document.createElement("script"); |
| el.defer = true; |
| el.async = false; |
| el.src = "dart_stack_trace_mapper.js"; |
| document.head.appendChild(el); |
| '''); |
| } |
| |
| entrypointJsContent.write(''' |
| el = document.createElement("script"); |
| el.defer = true; |
| el.async = false; |
| el.src = "require.js"; |
| el.setAttribute("data-main", "$bootstrapModuleName"); |
| document.head.appendChild(el); |
| '''); |
| outputCompleters[jsEntrypointId].complete( |
| new Asset.fromString(jsEntrypointId, entrypointJsContent.toString())); |
| |
| if (isDebug) { |
| outputCompleters[jsMapEntrypointId].complete(new Asset.fromString( |
| jsMapEntrypointId, |
| '{"version":3,"sourceRoot":"","sources":[],"names":[],"mappings":"",' |
| '"file":""}')); |
| } |
| }); |
| } |
| |
| /// Compiles [module] using the `dartdevc` binary from the SDK to a relative |
| /// path under the package that looks like `$outputDir/${module.id.name}.js`. |
| /// |
| /// Synchronously returns a `Map<AssetId, Future<Asset>>` so that you can know |
| /// immediately what assets will be output. |
| Map<AssetId, Future<Asset>> createDartdevcModule( |
| AssetId id, |
| ModuleReader moduleReader, |
| ScratchSpace scratchSpace, |
| Map<String, String> environmentConstants, |
| BarbackMode mode) { |
| assert(id.extension == '.js'); |
| var outputCompleters = <AssetId, Completer<Asset>>{ |
| id: new Completer(), |
| }; |
| var isDebug = mode == BarbackMode.DEBUG; |
| if (isDebug) { |
| outputCompleters[id.addExtension('.map')] = new Completer(); |
| } |
| |
| return _ensureComplete(outputCompleters, () async { |
| var module = await moduleReader.moduleFor(id); |
| var transitiveModuleDeps = await moduleReader.readTransitiveDeps(module); |
| var linkedSummaryIds = |
| transitiveModuleDeps.map((depId) => depId.linkedSummaryId).toSet(); |
| var allAssetIds = new Set<AssetId>() |
| ..addAll(module.assetIds) |
| ..addAll(linkedSummaryIds); |
| await scratchSpace.ensureAssets(allAssetIds); |
| var jsOutputFile = scratchSpace.fileFor(module.id.jsId); |
| var sdk_summary = p.url.join(sdkDir.path, 'lib/_internal/ddc_sdk.sum'); |
| var request = new WorkRequest(); |
| request.arguments.addAll([ |
| '--dart-sdk-summary=$sdk_summary', |
| '--modules=amd', |
| '--dart-sdk=${sdkDir.path}', |
| '--module-root=${scratchSpace.tempDir.path}', |
| '--library-root=${p.dirname(jsOutputFile.path)}', |
| '--summary-extension=${linkedSummaryExtension.substring(1)}', |
| '--no-summarize', |
| '-o', |
| jsOutputFile.path, |
| ]); |
| |
| if (isDebug) { |
| request.arguments.addAll([ |
| '--source-map', |
| '--source-map-comment', |
| '--inline-source-map', |
| ]); |
| } else { |
| request.arguments.add('--no-source-map'); |
| } |
| |
| // Add environment constants. |
| environmentConstants.forEach((key, value) { |
| request.arguments.add('-D$key=$value'); |
| }); |
| |
| // Add all the linked summaries as summary inputs. |
| for (var id in linkedSummaryIds) { |
| request.arguments.addAll(['-s', scratchSpace.fileFor(id).path]); |
| } |
| |
| // Add URL mappings for all the package: files to tell DartDevc where to |
| // find them. |
| for (var id in module.assetIds) { |
| var uri = canonicalUriFor(id); |
| if (uri.startsWith('package:')) { |
| request.arguments |
| .add('--url-mapping=$uri,${scratchSpace.fileFor(id).path}'); |
| } |
| } |
| // And finally add all the urls to compile, using the package: path for |
| // files under lib and the full absolute path for other files. |
| request.arguments.addAll(module.assetIds.map((id) { |
| var uri = canonicalUriFor(id); |
| if (uri.startsWith('package:')) { |
| return uri; |
| } |
| return scratchSpace.fileFor(id).path; |
| })); |
| |
| var response = await dartdevcDriver.doWork(request); |
| |
| // TODO(jakemac53): Fix the ddc worker mode so it always sends back a bad |
| // status code if something failed. Today we just make sure there is an output |
| // JS file to verify it was successful. |
| if (response.exitCode != EXIT_CODE_OK || !jsOutputFile.existsSync()) { |
| outputCompleters[module.id.jsId].completeError( |
| new DartDevcCompilationException(module.id.jsId, response.output)); |
| } else { |
| outputCompleters[module.id.jsId].complete( |
| new Asset.fromBytes(module.id.jsId, jsOutputFile.readAsBytesSync())); |
| if (isDebug) { |
| var sourceMapFile = scratchSpace.fileFor(module.id.jsSourceMapId); |
| outputCompleters[module.id.jsSourceMapId].complete(new Asset.fromBytes( |
| module.id.jsSourceMapId, sourceMapFile.readAsBytesSync())); |
| } |
| } |
| }); |
| } |
| |
| /// Copies the `dart_sdk.js` and `require.js` AMD files from the SDK into |
| /// [outputDir]. |
| /// |
| /// Returns a `Map<AssetId, Asset>` of the created assets. |
| Map<AssetId, Asset> copyDartDevcResources(String package, String outputDir) { |
| var sdk = cli_util.getSdkDir(); |
| var outputs = <AssetId, Asset>{}; |
| |
| // Copy the dart_sdk.js file for AMD into the output folder. |
| var sdkJsOutputId = |
| new AssetId(package, p.url.join(outputDir, 'dart_sdk.js')); |
| var sdkAmdJsPath = p.url.join(sdk.path, 'lib/dev_compiler/amd/dart_sdk.js'); |
| outputs[sdkJsOutputId] = |
| new Asset.fromFile(sdkJsOutputId, new File(sdkAmdJsPath)); |
| |
| // Copy the require.js file for AMD into the output folder. |
| var requireJsPath = p.url.join(sdk.path, 'lib/dev_compiler/amd/require.js'); |
| var requireJsOutputId = |
| new AssetId(package, p.url.join(outputDir, 'require.js')); |
| outputs[requireJsOutputId] = |
| new Asset.fromFile(requireJsOutputId, new File(requireJsPath)); |
| |
| return outputs; |
| } |
| |
| /// Runs [fn], and then ensures that all [completers] are completed. |
| /// |
| /// If an error is caught, then all [completers] that weren't completed are |
| /// completed with that error and stack trace. |
| /// |
| /// If no error was caught then all [completers] that weren't completed are |
| /// completed with an [AssetNotFoundException]. |
| /// |
| /// Synchronously returns a `Map<AssetId, Future<Asset>` which is derived from |
| /// the original [completers]. |
| Map<AssetId, Future<Asset>> _ensureComplete( |
| Map<AssetId, Completer<Asset>> completers, Future fn()) { |
| var futures = <AssetId, Future<Asset>>{}; |
| completers.forEach((k, v) => futures[k] = v.future); |
| () async { |
| try { |
| await fn(); |
| } catch (e, s) { |
| for (var completer in completers.values) { |
| if (!completer.isCompleted) completer.completeError(e, s); |
| } |
| } finally { |
| for (var id in completers.keys) { |
| if (!completers[id].isCompleted) { |
| completers[id].completeError(new AssetNotFoundException(id)); |
| } |
| } |
| } |
| }(); |
| return futures; |
| } |