blob: fd362089bfd7bd19fa3d25515f69c0ac545868e7 [file] [log] [blame]
// 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:shelf/shelf.dart';
import 'strategy.dart';
/// JavaScript snippet to determine the base URL of the current path.
const _baseUrlScript = '''
var baseUrl = (function () {
// Attempt to detect --precompiled mode for tests, and set the base url
// appropriately, otherwise set it to '/'.
var pathParts = location.pathname.split("/");
if (pathParts[0] == "") {
pathParts.shift();
}
if (pathParts.length > 1 && pathParts[1] == "test") {
return "/" + pathParts.slice(0, 2).join("/") + "/";
}
// Attempt to detect base url using <base href> html tag
// base href should start and end with "/"
if (typeof document !== 'undefined') {
var el = document.getElementsByTagName('base');
if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute
("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){
return el[0].getAttribute("href");
}
}
// return default value
return "/";
}());
''';
/// JavaScript snippet to determine the directory a script was run from.
const _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;
})();
''';
/// A load strategy for the require-js module system.
class RequireStrategy extends LoadStrategy {
@override
final ReloadConfiguration reloadConfiguration;
/// The module extension, e.g. `.ddc`.
final String _moduleExtension;
final String _requireDigestsPath = r'$requireDigestsPath';
/// Returns a map of module name to corresponding module path for the provided
/// Dart application entrypoint.
///
/// For example:
///
/// web/main -> main.ddc
/// packages/path/path -> packages/path/path.ddc
///
final Future<Map<String, String>> Function(String entrypoint) _moduleProvider;
/// Returns a map of module name to corresponding digest value.
///
/// For example:
///
/// web/main.ddc.js -> 8363b363f74b41cac955024ab8b94a3f
/// packages/path/path.ddc.js -> d348c2a4647e998011fe305f74f22961
///
final Future<Map<String, String>> Function(String entrypoint)
_digestsProvider;
RequireStrategy(
this.reloadConfiguration,
this._moduleExtension,
this._moduleProvider,
this._digestsProvider,
);
@override
Handler get handler => (request) async {
if (request.url.path.endsWith(_requireDigestsPath)) {
var digests =
await _digestsProvider(request.url.queryParameters['entrypoint']);
return Response.ok(json.encode(digests));
}
return null;
};
@override
String get id => 'require-js';
@override
String get loadLibrariesSnippet =>
'let libs = $loadModuleSnippet("dart_sdk").dart.getLibraries();\n';
@override
String get loadModuleSnippet => 'require';
/// Require JS config for ddc.
///
/// Sets the base url to `/` so that all modules can be loaded using absolute
/// paths which simplifies a lot of scenarios.
///
/// Sets the timeout for loading modules to infinity (0).
///
/// Sets up the custom module paths.
///
/// Adds error handler code for require.js which requests a `.errors` file for
/// any failed module, and logs it to the console.
String get _requireJsConfig => '''
$_baseUrlScript;
require.config({
baseUrl: baseUrl,
waitSeconds: 0,
paths: customModulePaths
});
const modulesGraph = new Map();
function getRegisteredModuleName(moduleMap) {
if (\$requireLoader.moduleIdToUrl.has(moduleMap.name + '$_moduleExtension')) {
return moduleMap.name + '$_moduleExtension';
}
return moduleMap.name;
}
requirejs.onResourceLoad = function (context, map, depArray) {
const name = getRegisteredModuleName(map);
const depNameArray = depArray.map(getRegisteredModuleName);
if (modulesGraph.has(name)) {
var previousDeps = modulesGraph.get(name);
var changed = previousDeps.length != depNameArray.length;
changed = changed || depNameArray.some(function(depName) {
return !previousDeps.includes(depName);
});
if (changed) {
console.warn("Dependencies graph change for module '" + name + "' detected. " +
"Dependencies was [" + previousDeps + "], now [" + depNameArray.map((depName) => depName) +"]. " +
"Page can't be hot-reloaded, firing full page reload.");
window.location.reload();
}
} else {
modulesGraph.set(name, []);
for (const depName of depNameArray) {
if (!\$requireLoader.moduleParentsGraph.has(depName)) {
\$requireLoader.moduleParentsGraph.set(depName, []);
}
\$requireLoader.moduleParentsGraph.get(depName).push(name);
modulesGraph.get(name).push(depName);
}
}
};
''';
@override
Future<String> bootstrapFor(String entrypoint) async =>
await _requireLoaderSetup(entrypoint) + _requireJsConfig;
@override
String loadClientSnippet(String clientScript) =>
'window.\$requireLoader.forceLoadModule("$clientScript");\n';
Future<String> _requireLoaderSetup(String entrypoint) async {
var modulePaths = await _moduleProvider(entrypoint);
return '''
$_currentDirectoryScript
$_baseUrlScript
let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)};
if(!window.\$requireLoader) {
window.\$requireLoader = {
digestsPath: '$_requireDigestsPath?entrypoint=$entrypoint',
moduleIdToUrl: new Map(),
urlToModuleId: new Map(),
rootDirectories: new Array(),
// Used in package:build_runner/src/server/build_updates_client/hot_reload_client.dart
moduleParentsGraph: new Map(),
moduleLoadingErrorCallbacks: new Map(),
forceLoadModule: function (moduleName, callback, onError) {
if (moduleName.endsWith('$_moduleExtension')) {
moduleName = moduleName.substring(0, moduleName.length - ${_moduleExtension.length});
}
if (typeof onError != 'undefined') {
var errorCallbacks = \$requireLoader.moduleLoadingErrorCallbacks;
if (!errorCallbacks.has(moduleName)) {
errorCallbacks.set(moduleName, new Set());
}
errorCallbacks.get(moduleName).add(onError);
}
requirejs.undef(moduleName);
requirejs([moduleName], function() {
if (typeof onError != 'undefined') {
errorCallbacks.get(moduleName).delete(onError);
}
if (typeof callback != 'undefined') {
callback();
}
});
},
getModuleLibraries: null, // set up by _initializeTools
};
}
let customModulePaths = {};
window.\$requireLoader.rootDirectories.push(window.location.origin + baseUrl);
for (let moduleName of Object.getOwnPropertyNames(modulePaths)) {
let modulePath = modulePaths[moduleName];
if (modulePath != moduleName) {
customModulePaths[moduleName] = modulePath;
}
var src = window.location.origin + '/' + modulePath + '.js';
if (window.\$requireLoader.moduleIdToUrl.has(moduleName)) {
continue;
}
\$requireLoader.moduleIdToUrl.set(moduleName, src);
\$requireLoader.urlToModuleId.set(src, moduleName);
}
''';
}
}