| // 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> |
| """; |
| } |
| |
| /* |
| Helper function to turn WebAssembly arrays into strings (which isn't possible |
| with JavaScript alone). Generated from this WAT module via `wasm-as -all` |
| and `base64 -w 0`: |
| |
| (module |
| (type $i8array (array (mut i8))) |
| (type $i16array (array (mut i16))) |
| (import "wasm:js-string" "fromCharCodeArray" |
| (func $fromCharCodeArray (param (ref null $i16array) i32 i32) (result (ref extern))) |
| ) |
| |
| (func (export "stringFromAsciiBytes") |
| (param $arr (ref $i8array)) |
| (param $start i32) |
| (param $length i32) |
| (result (ref extern)) |
| |
| (local $i i32) |
| (local $expanded (ref $i16array)) |
| |
| ;; Copy i8 array into an i16 array |
| (local.set $i (local.get $length)) |
| (local.set $expanded (array.new $i16array (i32.const 0) (local.get $length))) |
| |
| ;; do { i--; expanded[i] = arr[i]; } while (i >= start); |
| (block $break |
| (loop $loop |
| (local.set $i (i32.add (local.get $i) (i32.const -1))) |
| (br_if $break |
| (i32.lt_s |
| (local.get $i) |
| (local.get $start) |
| ) |
| ) |
| |
| (array.set $i16array |
| (local.get $expanded) |
| (local.get $i) |
| (array.get_u $i8array (local.get $arr) (local.get $i)) |
| ) |
| br $loop |
| ) |
| ) |
| |
| (call $fromCharCodeArray |
| (local.get $expanded) |
| (i32.const 0) |
| (array.len (local.get $expanded)) |
| ) |
| ) |
| |
| (func (export "stringFromCharCodeArray") |
| (param $arr (ref $i16array)) |
| (param $start i32) |
| (param $length i32) |
| (result (ref extern)) |
| |
| (call $fromCharCodeArray |
| (local.get $arr) |
| (local.get $start) |
| (i32.add |
| (local.get $start) |
| (local.get $length) |
| ) |
| ) |
| ) |
| ) |
| */ |
| const _wasmStandaloneArrayHelper = |
| 'AGFzbQEAAAABIgVedwFeeAFgA2MAf38BZG9gA2QBf38BZG9gA2QAf38BZG8CJAEOd2FzbTpqcy1zdHJpbmcRZnJvbUNoYXJDb2RlQXJyYXkAAgMDAgMEBzICFHN0cmluZ0Zyb21Bc2NpaUJ5dGVzAAEXc3RyaW5nRnJvbUNoYXJDb2RlQXJyYXkAAgpTAkMCAX8BZAAgAiEDQQAgAvsGACEEAkADQCADQX9qIQMgAyABSA0BIAQgAyAAIAP7DQH7DgAMAAsACyAEQQAgBPsPEAALDQAgACABIAEgAmoQAAs='; |
| |
| String dart2wasmHtml( |
| String title, |
| String wasmPath, |
| String mjsPath, |
| String supportJsPath, |
| bool standalone, |
| ) { |
| const standaloneEmbedder = |
| """ |
| const { instance: helperInstance } = await WebAssembly.instantiate(Uint8Array.fromBase64('$_wasmStandaloneArrayHelper'), {}, { |
| builtins: ['js-string'] |
| }); |
| |
| const dartEmbedder = { |
| // See sdk/lib/_internal/wasm_standalone/lib/embedder.dart for required definitions. |
| scheduleOnce: (delayInMicros, callback, arg) => { |
| const timeout = setTimeout(() => callback(arg), Number(delayInMicros / 1000n)); |
| return {timeout}; |
| }, |
| scheduleRepeated: (intervalMicros, callback, arg) => { |
| const timeout = setInterval(() => callback(arg), Number(intervalMicros / 1000n)); |
| return {timeout}; |
| }, |
| queueMicrotask: (callback, arg) => { |
| queueMicrotask(() => callback(arg)); |
| }, |
| clearSchedule: (schedule) => { |
| clearTimeout(schedule.timeout); |
| }, |
| currentTime: () => BigInt(Date.now()) * 1000n, |
| stringFromAsciiBytes: (chars, start, length) => { |
| const str = helperInstance.exports.stringFromAsciiBytes(chars, start, length); |
| return str; |
| }, |
| stringFromCharCodeArray: (chars, start, length) => { |
| const str = helperInstance.exports.stringFromCharCodeArray(chars, start, length); |
| return str; |
| }, |
| monotonicClockFrequency: () => 1_000_000, |
| monotonicClockTicks: () => BigInt(Math.round(performance.now() * 1000)), |
| weakRefCreate: (dartValue) => new WeakRef(dartValue), |
| weakRefGet: (weakRef) => weakRef.deref() ?? null, |
| expandoCreate: () => new WeakMap(), |
| expandoGet: (expando, target, _hash) => expando.get(target) ?? null, |
| expandoSet: (expando, target, _hash, value) => expando.set(target, value), |
| finalizerCreate: (callback, firstParameter) => { |
| return new FinalizationRegistry((heldValue) => { |
| callback(heldValue, firstParameter); |
| }); |
| }, |
| finalizerAttach: (finalizer, object, token, detachToken) => { |
| if (detachToken) { |
| finalizer.register(object, token, detachToken); |
| } else { |
| finalizer.register(object, token); |
| } |
| }, |
| finalizerDetach: (finalizer, detachToken) => finalizer.unregister(detachToken), |
| baseUri: () => globalThis.location.href, |
| isWindows: () => false, |
| stackTraceGetCurrent: () => new Error().stack, |
| stackTraceToString: (trace) => { |
| const stackString = trace.toString(); |
| const frames = stackString.split('\\n'); |
| // Format of stack traces is: |
| // 1. stackTraceGetCurrent (from this embedder object) |
| // 2. module0.StackTrace.current <noInline> |
| // 3. The callsite we care about. |
| const drop = 1 + frames.findIndex((l) => l.indexOf('stackTraceGetCurrent') > 0); |
| return frames.slice(drop).join('\\n'); |
| }, |
| doubleTryParse: (source) => { |
| if (!/${r'^\s*[+-]?(?:Infinity|NaN|(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$'}/.test(source)) { |
| const trimmed = source.trim(); |
| // parseFloat is more lenient than double.tryParse, see wasm/lib/double_patch.dart for details. |
| if (!(trimmed == 'NaN' || trimmed == '+NaN' || trimmed == '-NaN')) { |
| return null; |
| } |
| return { result: NaN }; |
| } else { |
| return { result: parseFloat(source) }; |
| } |
| }, |
| tryParseResultGetDouble: ({result}) => result, |
| i64ToString: (source, radix) => source.toString(radix), |
| f64ToExponential: (source) => source.toExponential(), |
| f64ToExponentialWithFractionDigits: (source, digits) => { |
| return source.toExponential(digits); |
| }, |
| f64ToPrecision: (source, digits) => source.toPrecision(digits), |
| f64ToFixed: (source, digits) => source.toFixed(digits), |
| f64ToString: (source) => { |
| if (Object.is(source, -0)) return '-0.0'; |
| if (Number.isNaN(source)) return 'NaN'; |
| if (source == Number.NEGATIVE_INFINITY) return '-Infinity'; |
| if (source == Number.POSITIVE_INFINITY) return 'Infinity'; |
| |
| let formatted = source.toString(); |
| if (source % 1.0 == 0 && formatted.indexOf('e') == -1) { |
| formatted += '.0'; |
| } |
| return formatted; |
| }, |
| stringBufferCreate: () => ({ contents: '' }), |
| stringBufferWriteString: (buffer, append) => { |
| buffer.contents += append; |
| }, |
| stringBufferWriteCharCode: (buffer, code) => { |
| buffer.contents += String.fromCodePoint(code); |
| }, |
| stringBufferClear: (buffer) => { |
| buffer.contents = '' |
| }, |
| stringBufferLength: (buffer) => buffer.contents.length, |
| stringBufferToString: ({contents}) => contents, |
| regexpCreateOrFailWithString: (pattern, multiLine, caseSensitive, unicode, dotAll) => { |
| let flags = ''; |
| if (multiLine) flags += 'm'; |
| if (!caseSensitive) flags += 'i'; |
| if (unicode) flags += 'u'; |
| if (dotAll) flags += 's'; |
| |
| try { |
| // Prepare two regular expressions, one for regular matches and one |
| // for matchAsPrefix. |
| return { |
| regular: new RegExp(pattern, flags + 'g'), |
| asPrefix: new RegExp(pattern, flags + 'y'), |
| }; |
| } catch (e) { |
| return String(e); |
| } |
| }, |
| regexpIsRegexp: (source) => typeof(source) !== 'string', |
| regexpEscape: (source) => { |
| // Note: We can't use RegExp.escape here, it escapes too much and we |
| // have tests expecting that e.g. \t isn't escaped. |
| if (/${r'[[\]{}()*+?.\\^$|]'}/.test(source)) { |
| return source.replace(/${r'[[\]{}()*+?.\\^$|]'}/g, "${r'\\$&'}"); |
| } else { |
| return source; |
| } |
| }, |
| regexpMatch: (regexp, string, start, asPrefix) => { |
| const regex = asPrefix ? regexp.asPrefix : regexp.regular; |
| regex.lastIndex = start; |
| const match = regex.exec(string); |
| if (match) { |
| return { |
| start: match.index, |
| end: match.index + match[0].length, |
| groupNames: match.groups ? Object.keys(match.groups) : [], |
| groups: match, |
| }; |
| } else { |
| return null; |
| } |
| }, |
| regexpMatchGetStart: (match) => match.start, |
| regexpMatchGetEnd: (match) => match.end, |
| regexpMatchGetGroupCount: (match) => match.groups.length - 1, |
| regexpMatchGetGroup: (match, index) => match.groups[index] ?? null, |
| regexpMatchGetNamedGroups: (match) => match.groupNames.length, |
| regexpMatchGetGroupName: (match, index) => match.groupNames[index], |
| regexpMatchGetGroupByName: (match, index) => match.groups.groups[match.groupNames[index]] ?? null, |
| timeZoneNameForClampedSeconds: (secondsSinceEpoch) => { |
| const date = new Date(Number(secondsSinceEpoch * 1000n)); |
| const match = /\\((.*)\\)/.exec(date.toString()); |
| if (match == null) { |
| // This should never happen on any recent browser. |
| return ''; |
| } |
| return match[1]; |
| }, |
| timeZoneOffsetInSecondsForClampedSeconds: (secondsSinceEpoch) => { |
| const date = new Date(Number(secondsSinceEpoch * 1000n)); |
| // This needs to be negated because Dart wants the difference between |
| // local time and UTC. |
| return -date.getTimezoneOffset() * 60; |
| }, |
| }; |
| """; |
| final additionalImports = standalone ? '{ dart: dartEmbedder }' : '{}'; |
| |
| 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"> |
| // Default stack trace limit in V8 is 10, which hides some of the stack frames |
| // we check in stack trace tests. |
| Error.stackTraceLimit = 20; |
| 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(); |
| }; |
| ${standalone ? standaloneEmbedder : ''} |
| const appInstance = await compiledApp.instantiate($additionalImports, { |
| loadDeferredModules: (modules, handleWasmBytes) => |
| Promise.all(modules.map((m) => fetch(m).then((b) => handleWasmBytes(m, b)))), |
| }); |
| dartMainRunner(() => { |
| appInstance.invokeMain(); |
| }); |
| } |
| |
| loadAndRun('$mjsPath', '$wasmPath', '$supportJsPath'); |
| </script> |
| </body> |
| </html>"""; |
| } |