blob: c498599183f5cdd8d33fcb2413f44d66a6c79211 [file] [log] [blame]
// Copyright (c) 2011, 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 'package:path/path.dart' as p;
import 'configuration.dart' show Compiler;
import 'utils.dart';
// The native JavaScript Object prototype is sealed before loading the Dart
// SDK module to guard against prototype pollution.
final _sealNativeObjectScript =
'/root_dart/sdk/lib/_internal/js_runtime/lib/preambles/'
'seal_native_object.js';
String dart2jsHtml(String title, String scriptPath) {
return """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<meta name="dart.unittest" content="full-stack-traces">
<title> Test $title </title>
<style>
.unittest-table { font-family:monospace; border:1px; }
.unittest-pass { background: #6b3;}
.unittest-fail { background: #d55;}
.unittest-error { background: #a11;}
</style>
<script type="text/javascript" src="$_sealNativeObjectScript"></script>
</head>
<body>
<h1> Running $title </h1>
<script type="text/javascript"
src="/root_dart/pkg/test_runner/lib/src/test_controller.js">
</script>
<script type="text/javascript" src="$scriptPath"
onerror="scriptTagOnErrorCallback(null)"
defer>
</script>
</body>
</html>""";
}
/// Transforms a path to a valid JS identifier.
///
/// This logic must be synchronized with [pathToJSIdentifier] in DDC at:
/// pkg/dev_compiler/lib/src/compiler/module_builder.dart
String pathToJSIdentifier(String path) {
path = p.normalize(path);
if (path.startsWith('/') || path.startsWith('\\')) {
path = path.substring(1, path.length);
}
return _toJSIdentifier(path
.replaceAll('\\', '__')
.replaceAll('/', '__')
.replaceAll('..', '__')
.replaceAll('-', '_'));
}
final _digitPattern = RegExp(r'\d');
/// Escape [name] to make it into a valid identifier.
String _toJSIdentifier(String name) {
if (name.isEmpty) return r'$';
// Escape any invalid characters
var result = name.replaceAllMapped(
_invalidCharInIdentifier, (match) => '\$${match[0]!.codeUnits.join("")}');
// Ensure the identifier first character is not numeric and that the whole
// identifier is not a keyword.
if (result.startsWith(_digitPattern) || _invalidVariableName(result)) {
return '\$$result';
}
return result;
}
// Invalid characters for identifiers, which would need to be escaped.
final _invalidCharInIdentifier = RegExp(r'[^A-Za-z_\d]');
bool _invalidVariableName(String keyword, {bool strictMode = true}) {
switch (keyword) {
// http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words
case "await":
case "break":
case "case":
case "catch":
case "class":
case "const":
case "continue":
case "debugger":
case "default":
case "delete":
case "do":
case "else":
case "enum":
case "export":
case "extends":
case "finally":
case "for":
case "function":
case "if":
case "import":
case "in":
case "instanceof":
case "let":
case "new":
case "return":
case "super":
case "switch":
case "this":
case "throw":
case "try":
case "typeof":
case "var":
case "void":
case "while":
case "with":
return true;
case "arguments":
case "eval":
// http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words
// http://www.ecma-international.org/ecma-262/6.0/#sec-identifiers-static-semantics-early-errors
case "implements":
case "interface":
case "package":
case "private":
case "protected":
case "public":
case "static":
case "yield":
return strictMode;
}
return false;
}
/// Generates the HTML template file needed to load and run a ddc test in
/// the browser.
///
/// [testName] is the short name of the test without any subdirectory path
/// or extension, like "math_test". [testNameAlias] is the alias of the
/// test variable used for import/export (usually relative to its module root).
/// [testJSDir] is the relative path to the build directory where the
/// ddc-generated JS file is stored. [ddcModuleFormat] determines whether to
/// emit a template that works with the DDC module format or one that works with
/// the AMD module format. [canaryMode] is whether DDC is running in canary
/// mode. If this flag and [ddcModuleFormat] is enabled, a template that works
/// with the DDC hot reload format will be emitted.
String ddcHtml(
String testName,
String testNameAlias,
String testJSDir,
Compiler compiler,
String genDir,
bool nativeNonNullAsserts,
bool jsInteropNonNullAsserts,
{bool ddcModuleFormat = false,
bool canaryMode = false}) {
var testId = pathToJSIdentifier(testName);
var testIdAlias = pathToJSIdentifier(testNameAlias);
var ddcGenDir = '/root_build/$genDir';
var hotReloadFormat = ddcModuleFormat && canaryMode;
var sdkAndAsyncHelperSetup = """
_debugger.registerDevtoolsFormatter();
testErrorToStackTrace = function(error) {
var stackTrace = getStackTraceString(error);
var lines = stackTrace.split("\\n");
// Remove the first line, which is just "Error".
lines = lines.slice(1);
// Strip off all of the lines for the bowels of the test runner.
for (var i = 0; i < lines.length; i++) {
if (lines[i].indexOf("dartMainRunner") != -1) {
lines = lines.slice(0, i);
break;
}
}
// TODO(rnystrom): It would be nice to shorten the URLs of the remaining
// lines too.
return lines.join("\\n");
};
""";
var sdkFlagSetup = """
runtime.nativeNonNullAsserts($nativeNonNullAsserts);
runtime.jsInteropNonNullAsserts($jsInteropNonNullAsserts);
""";
String script;
if (ddcModuleFormat) {
var appName = '/root_dart/$testJSDir';
// Used in the DDC module system for multi-app workflows, and are simply
// placeholder values here.
var uuid = '00000000-0000-0000-0000-000000000000';
var loadPackagesScript = [
for (var p in testPackages)
"""<script defer type="text/javascript"
src="$ddcGenDir/pkg/ddc/$p.js"></script>"""
].join('\n');
String libraryImports;
String startCode;
if (hotReloadFormat) {
libraryImports = """
let _debugger = dartDevEmbedder.debugger;
let getStackTraceString = function(error) {
return _debugger.stackTrace(error);
}
""";
sdkFlagSetup = """
let sdkOptions = {
nativeNonNullAsserts: $nativeNonNullAsserts,
jsInteropNonNullAsserts: $jsInteropNonNullAsserts,
};
""";
startCode =
"""dartDevEmbedder.runMain("org-dartlang-app:/$testNameAlias.dart",
sdkOptions);""";
} else {
libraryImports = """
let sdk = dart_library.import("dart_sdk", "$appName");
let runtime = sdk.dart;
let getStackTraceString = function(error) {
return runtime.stackTrace(error).toString();
}
let _debugger = sdk._debugger;
""";
startCode = """dart_library.start("$appName", "$uuid", "$testName",
"$testIdAlias", false)""";
}
script = """
<script defer type="text/javascript"
src="/root_dart/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js"></script>
<script defer type="text/javascript" src="$ddcGenDir/sdk/ddc/dart_sdk.js"></script>
$loadPackagesScript
<script defer type="text/javascript" src="$appName/$testName.js"></script>
<script type="text/javascript">"""
// DDC module format doesn't defer the execution until the document is finished
// parsing. We can defer scripts, but only if they are in separate files and not
// inline JS like below. In order to make sure everything is loaded and be
// consistent with the AMD module format, we should wait until a
// `DOMContentLoaded` event is fired. Other options are using `type = "module"`
// or putting this in a separate JS file, but this is the simplest solution.
"""
document.addEventListener("DOMContentLoaded", (e) => {
$libraryImports
$sdkAndAsyncHelperSetup
$sdkFlagSetup
dartMainRunner(function () {
return $startCode;
});
});
</script>
""";
} else {
var packagePaths = [
for (var p in testPackages) ' "$p": "$ddcGenDir/pkg/amd/$p",'
].join("\n");
script = """
<script>
var require = {
baseUrl: "/root_dart/$testJSDir",
paths: {
"dart_sdk": "$ddcGenDir/sdk/amd/dart_sdk",
$packagePaths
},
waitSeconds: 45,
};
</script>
<script type="text/javascript"
src="/root_dart/third_party/requirejs/require.js"></script>
<script type="text/javascript">
requirejs(["$testName", "dart_sdk"], function($testId, sdk) {
let runtime = sdk.dart;
let getStackTraceString = function(error) {
return runtime.stackTrace(error).toString();
}
let _debugger = sdk._debugger;
$sdkAndAsyncHelperSetup
$sdkFlagSetup
dartMainRunner(function testMainWrapper() {
return $testId.$testIdAlias.main();
});
});
</script>
""";
}
return """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<meta name="dart.unittest" content="full-stack-traces">
<title>Test $testName</title>
<style>
.unittest-table { font-family:monospace; border:1px; }
.unittest-pass { background: #6b3;}
.unittest-fail { background: #d55;}
.unittest-error { background: #a11;}
</style>
<script type="text/javascript" src="$_sealNativeObjectScript"></script>
</head>
<body>
<h1>Running $testName</h1>
<script type="text/javascript"
src="/root_dart/pkg/test_runner/lib/src/test_controller.js">
</script>
$script
</body>
</html>
""";
}
String dart2wasmHtml(
String title, String wasmPath, String mjsPath, String supportJsPath) {
return """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<meta name="dart.unittest" content="full-stack-traces">
<title> Test $title </title>
<link rel="preload" href="$wasmPath" as="fetch" crossorigin>
<style>
.unittest-table { font-family:monospace; border:1px; }
.unittest-pass { background: #6b3;}
.unittest-fail { background: #d55;}
.unittest-error { background: #a11;}
</style>
</head>
<body>
<h1> Running $title </h1>
<script type="text/javascript"
src="/root_dart/pkg/test_runner/lib/src/test_controller.js">
</script>
<script type="module">
async function loadAndRun(mjsPath, wasmPath, supportJsPath) {
const supportJSExpression = await (await fetch(supportJsPath)).text();
const support = eval(supportJSExpression);
if (support !== true) {
dartMainRunner(() => {
throw 'This browser does not support the required features to run the dart2wasm compiled app!';
});
return;
}
const mjs = await import(mjsPath);
const compiledApp = await mjs.compileStreaming(fetch(wasmPath));
window.loadData = async (relativeToWasmFileUri) => {
const path = '$wasmPath'.slice(0, wasmPath.lastIndexOf('/'));
const response = await fetch(`\${path}/\${relativeToWasmFileUri}`);
return response.arrayBuffer();
};
const appInstance = await compiledApp.instantiate({}, {
loadDeferredWasm: (moduleName) => {
const moduleFile = '$wasmPath'.replace('.wasm', `_\${moduleName}.wasm`);
return fetch(moduleFile);
}
});
dartMainRunner(() => {
appInstance.invokeMain();
});
}
loadAndRun('$mjsPath', '$wasmPath', '$supportJsPath');
</script>
</body>
</html>""";
}