Refactor flutter.js to do dart2wasm bootstrapping and CanvasKit/Skwasm preloading. (#49037)
This PR makes some major revisions to our flutter.js bootstrapper.
* Modularize flutter.js into multiple files to make it a little simpler to manage from source code. They are still bundled into a single .js file by esbuild.
* Added a `types.d.ts` file which contains declarations of the types of some of the objects used in the flutter.js API
* Deprecated the old `FlutterLoader.loadEntrypoint` API and added a new function simply called `FlutterLoader.load`, which has a few more capabilities:
- A build tool can inject a build config, that may describe multiple builds that `FlutterLoader.load` can attempt to use. It will use the first one that is compatible with the browser environment and the user's configuration.
- It can also load wasm flutter apps.
- It also pre-loads and instantiates CanvasKit (and Skwasm) as necessary depending on the build configuration.
- `FlutterLoader.load` also immediately takes a flutter configuration object. If an `onEntrypointLoaded` callback is not provided by the user, it just does the expected thing and initializes the engine and immediately starts the app, passing the configuration along as needed.
* `flutter.js` has the engine hash built into it now, which allows it to ascertain the correct CDN URLs for both CanvasKit and Skwasm.
diff --git a/ci/builders/linux_web_engine.json b/ci/builders/linux_web_engine.json
index 9dc023b..138e425 100644
--- a/ci/builders/linux_web_engine.json
+++ b/ci/builders/linux_web_engine.json
@@ -179,28 +179,6 @@
}
},
{
- "name": "web_tests/test_bundles/dart2js-skwasm-skwasm_stub",
- "drone_dimensions": [
- "device_type=none",
- "os=Linux"
- ],
- "generators": {
- "tasks": [
- {
- "name": "compile bundle dart2js-skwasm-skwasm_stub",
- "parameters": [
- "test",
- "--compile",
- "--bundle=dart2js-skwasm-skwasm_stub"
- ],
- "scripts": [
- "flutter/lib/web_ui/dev/felt"
- ]
- }
- ]
- }
- },
- {
"name": "web_tests/test_bundles/dart2wasm-html-engine",
"drone_dimensions": [
"device_type=none",
@@ -331,6 +309,28 @@
}
]
}
+ },
+ {
+ "name": "web_tests/test_bundles/fallbacks",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Linux"
+ ],
+ "generators": {
+ "tasks": [
+ {
+ "name": "compile bundle fallbacks",
+ "parameters": [
+ "test",
+ "--compile",
+ "--bundle=fallbacks"
+ ],
+ "scripts": [
+ "flutter/lib/web_ui/dev/felt"
+ ]
+ }
+ ]
+ }
}
],
"tests": [
@@ -515,42 +515,6 @@
]
},
{
- "name": "Linux run chrome-dart2js-skwasm-skwasm_stub suite",
- "recipe": "engine_v2/tester_engine",
- "drone_dimensions": [
- "device_type=none",
- "os=Linux"
- ],
- "gclient_variables": {
- "download_android_deps": false
- },
- "dependencies": [
- "web_tests/artifacts",
- "web_tests/test_bundles/dart2js-skwasm-skwasm_stub"
- ],
- "test_dependencies": [
- {
- "dependency": "goldctl",
- "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
- },
- {
- "dependency": "chrome_and_driver",
- "version": "119.0.6045.9"
- }
- ],
- "tasks": [
- {
- "name": "run suite chrome-dart2js-skwasm-skwasm_stub",
- "parameters": [
- "test",
- "--run",
- "--suite=chrome-dart2js-skwasm-skwasm_stub"
- ],
- "script": "flutter/lib/web_ui/dev/felt"
- }
- ]
- },
- {
"name": "Linux run chrome-full-dart2js-canvaskit-canvaskit suite",
"recipe": "engine_v2/tester_engine",
"drone_dimensions": [
@@ -1091,6 +1055,78 @@
]
},
{
+ "name": "Linux run chrome-fallbacks suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Linux"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/fallbacks"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-fallbacks",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-fallbacks"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Linux run firefox-fallbacks suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Linux"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/fallbacks"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "firefox",
+ "version": "version:106.0"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite firefox-fallbacks",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=firefox-fallbacks"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
"name": "Mac run safari-dart2js-html-engine suite",
"recipe": "engine_v2/tester_engine",
"drone_dimensions": [
@@ -1256,6 +1292,39 @@
]
},
{
+ "name": "Mac run safari-fallbacks suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Mac-13",
+ "cpu=arm64"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/fallbacks"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite safari-fallbacks",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=safari-fallbacks"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
"name": "Windows run chrome-dart2js-html-engine suite",
"recipe": "engine_v2/tester_engine",
"drone_dimensions": [
@@ -1436,42 +1505,6 @@
]
},
{
- "name": "Windows run chrome-dart2js-skwasm-skwasm_stub suite",
- "recipe": "engine_v2/tester_engine",
- "drone_dimensions": [
- "device_type=none",
- "os=Windows"
- ],
- "gclient_variables": {
- "download_android_deps": false
- },
- "dependencies": [
- "web_tests/artifacts",
- "web_tests/test_bundles/dart2js-skwasm-skwasm_stub"
- ],
- "test_dependencies": [
- {
- "dependency": "goldctl",
- "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
- },
- {
- "dependency": "chrome_and_driver",
- "version": "119.0.6045.9"
- }
- ],
- "tasks": [
- {
- "name": "run suite chrome-dart2js-skwasm-skwasm_stub",
- "parameters": [
- "test",
- "--run",
- "--suite=chrome-dart2js-skwasm-skwasm_stub"
- ],
- "script": "flutter/lib/web_ui/dev/felt"
- }
- ]
- },
- {
"name": "Windows run chrome-full-dart2js-canvaskit-canvaskit suite",
"recipe": "engine_v2/tester_engine",
"drone_dimensions": [
@@ -1542,6 +1575,330 @@
"script": "flutter/lib/web_ui/dev/felt"
}
]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-html-engine suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-html-engine"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-html-engine",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-html-engine"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-html-html suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-html-html"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-html-html",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-html-html"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-html-ui suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-html-ui"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-html-ui",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-html-ui"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-canvaskit-canvaskit suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-canvaskit-canvaskit"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-canvaskit-canvaskit",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-canvaskit-canvaskit"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-canvaskit-ui suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-canvaskit-ui"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-canvaskit-ui",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-canvaskit-ui"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-dart2wasm-skwasm-ui suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-skwasm-ui"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-dart2wasm-skwasm-ui",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-dart2wasm-skwasm-ui"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-full-dart2wasm-canvaskit-canvaskit suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-canvaskit-canvaskit"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-full-dart2wasm-canvaskit-canvaskit",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-full-dart2wasm-canvaskit-canvaskit"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-full-dart2wasm-canvaskit-ui suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/dart2wasm-canvaskit-ui"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-full-dart2wasm-canvaskit-ui",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-full-dart2wasm-canvaskit-ui"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
+ },
+ {
+ "name": "Windows run chrome-fallbacks suite",
+ "recipe": "engine_v2/tester_engine",
+ "drone_dimensions": [
+ "device_type=none",
+ "os=Windows"
+ ],
+ "gclient_variables": {
+ "download_android_deps": false
+ },
+ "dependencies": [
+ "web_tests/artifacts",
+ "web_tests/test_bundles/fallbacks"
+ ],
+ "test_dependencies": [
+ {
+ "dependency": "goldctl",
+ "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+ },
+ {
+ "dependency": "chrome_and_driver",
+ "version": "119.0.6045.9"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "run suite chrome-fallbacks",
+ "parameters": [
+ "test",
+ "--run",
+ "--suite=chrome-fallbacks"
+ ],
+ "script": "flutter/lib/web_ui/dev/felt"
+ }
+ ]
}
]
}
\ No newline at end of file
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 7a9bb3f..8ab8045 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -5877,7 +5877,16 @@
ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/base_uri.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/canvaskit_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/entrypoint_loader.js + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/flutter.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/instantiate_wasm.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/service_worker_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/skwasm_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/trusted_types.js + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/annotations.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/canvas.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/channel_buffers.dart + ../../../flutter/LICENSE
@@ -8700,7 +8709,17 @@
FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h
FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc
FILE: ../../../flutter/lib/ui/window/viewport_metrics.h
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/base_uri.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/canvaskit_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/entrypoint_loader.js
FILE: ../../../flutter/lib/web_ui/flutter_js/src/flutter.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/instantiate_wasm.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/service_worker_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/trusted_types.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/types.d.ts
FILE: ../../../flutter/lib/web_ui/lib/annotations.dart
FILE: ../../../flutter/lib/web_ui/lib/canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/channel_buffers.dart
diff --git a/lib/web_ui/dev/chrome.dart b/lib/web_ui/dev/chrome.dart
index 374d4d6..b209ddf 100644
--- a/lib/web_ui/dev/chrome.dart
+++ b/lib/web_ui/dev/chrome.dart
@@ -23,14 +23,11 @@
/// Provides an environment for desktop Chrome.
class ChromeEnvironment implements BrowserEnvironment {
ChromeEnvironment({
- required bool enableWasmGC,
required bool useDwarf,
- }) : _enableWasmGC = enableWasmGC,
- _useDwarf = useDwarf;
+ }) : _useDwarf = useDwarf;
late final BrowserInstallation _installation;
- final bool _enableWasmGC;
final bool _useDwarf;
@override
@@ -42,7 +39,6 @@
url,
_installation,
debug: debug,
- enableWasmGC: _enableWasmGC,
useDwarf: _useDwarf
);
}
@@ -83,7 +79,6 @@
Uri url,
BrowserInstallation installation, {
required bool debug,
- required bool enableWasmGC,
required bool useDwarf,
}) {
final Completer<Uri> remoteDebuggerCompleter = Completer<Uri>.sync();
@@ -101,13 +96,7 @@
final bool isChromeNoSandbox =
Platform.environment['CHROME_NO_SANDBOX'] == 'true';
final String dir = await generateUserDirectory(installation, useDwarf);
- final String jsFlags = enableWasmGC ? <String>[
- '--experimental-wasm-gc',
- '--experimental-wasm-stack-switching',
- '--experimental-wasm-type-reflection',
- ].join(' ') : '';
final List<String> args = <String>[
- if (jsFlags.isNotEmpty) '--js-flags=$jsFlags',
'--user-data-dir=$dir',
url.toString(),
if (!debug)
diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart
index 301b9bc..0eabc3b 100644
--- a/lib/web_ui/dev/common.dart
+++ b/lib/web_ui/dev/common.dart
@@ -240,12 +240,11 @@
/// The [browserName] matches the browser name passed as the `--browser` option.
BrowserEnvironment getBrowserEnvironment(
BrowserName browserName, {
- required bool enableWasmGC,
required bool useDwarf,
}) {
switch (browserName) {
case BrowserName.chrome:
- return ChromeEnvironment(enableWasmGC: enableWasmGC, useDwarf: useDwarf);
+ return ChromeEnvironment(useDwarf: useDwarf);
case BrowserName.edge:
return EdgeEnvironment();
case BrowserName.firefox:
diff --git a/lib/web_ui/dev/felt_config.dart b/lib/web_ui/dev/felt_config.dart
index 6e42e50..31f8739 100644
--- a/lib/web_ui/dev/felt_config.dart
+++ b/lib/web_ui/dev/felt_config.dart
@@ -32,11 +32,11 @@
}
class TestBundle {
- TestBundle(this.name, this.testSet, this.compileConfig);
+ TestBundle(this.name, this.testSet, this.compileConfigs);
final String name;
final TestSet testSet;
- final CompileConfiguration compileConfig;
+ final List<CompileConfiguration> compileConfigs;
}
enum CanvasKitVariant {
@@ -157,12 +157,16 @@
if (testSet == null) {
throw AssertionError('Test set not found with name: `$testSetName` (referenced by test bundle: `$name`)');
}
- final String compileConfigName = testBundleYaml['compile-config'] as String;
- final CompileConfiguration? compileConfig = compileConfigsByName[compileConfigName];
- if (compileConfig == null) {
- throw AssertionError('Compile config not found with name: `$compileConfigName` (referenced by test bundle: `$name`)');
+ final dynamic compileConfigsValue = testBundleYaml['compile-configs'];
+ final List<CompileConfiguration> compileConfigs;
+ if (compileConfigsValue is String) {
+ compileConfigs = <CompileConfiguration>[compileConfigsByName[compileConfigsValue]!];
+ } else {
+ compileConfigs = (compileConfigsValue as List<dynamic>).map(
+ (dynamic configName) => compileConfigsByName[configName as String]!
+ ).toList();
}
- final TestBundle bundle = TestBundle(name, testSet, compileConfig);
+ final TestBundle bundle = TestBundle(name, testSet, compileConfigs);
testBundles.add(bundle);
if (testBundlesByName.containsKey(name)) {
throw AssertionError('Duplicate test bundle name: $name');
@@ -202,11 +206,6 @@
if (runConfig == null) {
throw AssertionError('Run config not found with name: `$runConfigName` (referenced by test suite: `$name`)');
}
- if (bundle.compileConfig.renderer == Renderer.canvaskit && runConfig.variant == null) {
- throw AssertionError(
- 'Run config `$runConfigName` was used with a CanvasKit test bundle `$testBundleName` '
- 'but did not specify a CanvasKit variant (referenced by test suite: `$name`)');
- }
bool canvasKit = false;
bool canvasKitChromium = false;
bool skwasm = false;
diff --git a/lib/web_ui/dev/generate_builder_json.dart b/lib/web_ui/dev/generate_builder_json.dart
index fe1e6f6..b2d9a5a 100644
--- a/lib/web_ui/dev/generate_builder_json.dart
+++ b/lib/web_ui/dev/generate_builder_json.dart
@@ -120,11 +120,7 @@
suite.runConfig.browser == BrowserName.safari
),
..._getTestStepsForPlatform(suites, 'Windows', (TestSuite suite) =>
- suite.runConfig.browser == BrowserName.chrome &&
-
- // TODO(jacksongardner): Enable dart2wasm tests on Windows
- // https://github.com/flutter/flutter/issues/124082
- suite.testBundle.compileConfig.compiler != Compiler.dart2wasm
+ suite.runConfig.browser == BrowserName.chrome
),
];
}
diff --git a/lib/web_ui/dev/steps/compile_bundle_step.dart b/lib/web_ui/dev/steps/compile_bundle_step.dart
index 6601e2f..b1d86ce 100644
--- a/lib/web_ui/dev/steps/compile_bundle_step.dart
+++ b/lib/web_ui/dev/steps/compile_bundle_step.dart
@@ -61,20 +61,20 @@
.toList();
}
- TestCompiler _createCompiler() {
- switch (bundle.compileConfig.compiler) {
+ TestCompiler _createCompiler(CompileConfiguration config) {
+ switch (config.compiler) {
case Compiler.dart2js:
return Dart2JSCompiler(
testSetDirectory,
outputBundleDirectory,
- renderer: bundle.compileConfig.renderer,
+ renderer: config.renderer,
isVerbose: isVerbose,
);
case Compiler.dart2wasm:
return Dart2WasmCompiler(
testSetDirectory,
outputBundleDirectory,
- renderer: bundle.compileConfig.renderer,
+ renderer: config.renderer,
isVerbose: isVerbose,
);
}
@@ -84,7 +84,9 @@
Future<void> run() async {
print('Compiling test bundle ${bundle.name.ansiMagenta}...');
final List<FilePath> allTests = _findTestFiles();
- final TestCompiler compiler = _createCompiler();
+ final List<TestCompiler> compilers = bundle.compileConfigs.map(
+ (CompileConfiguration config) => _createCompiler(config)
+ ).toList();
final Stopwatch stopwatch = Stopwatch()..start();
final String testSetDirectoryPath = testSetDirectory.path;
@@ -94,26 +96,28 @@
}
final List<Future<MapEntry<String, CompileResult>>> pendingResults = <Future<MapEntry<String, CompileResult>>>[];
- for (final FilePath testFile in allTests) {
- final String relativePath = pathlib.relative(
- testFile.absolute,
- from: testSetDirectoryPath);
- final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async {
- if (testFiles != null && !testFiles!.contains(testFile)) {
- return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered);
- }
- final bool success = await compiler.compileTest(testFile);
- const int maxTestNameLength = 80;
- final String truncatedPath = relativePath.length > maxTestNameLength
- ? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...')
- : relativePath;
- final String expandedPath = truncatedPath.padRight(maxTestNameLength);
- io.stdout.write('\r ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}');
- return success
- ? MapEntry<String, CompileResult>(relativePath, CompileResult.success)
- : MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure);
- });
- pendingResults.add(result);
+ for (final TestCompiler compiler in compilers) {
+ for (final FilePath testFile in allTests) {
+ final String relativePath = pathlib.relative(
+ testFile.absolute,
+ from: testSetDirectoryPath);
+ final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async {
+ if (testFiles != null && !testFiles!.contains(testFile)) {
+ return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered);
+ }
+ final bool success = await compiler.compileTest(testFile);
+ const int maxTestNameLength = 80;
+ final String truncatedPath = relativePath.length > maxTestNameLength
+ ? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...')
+ : relativePath;
+ final String expandedPath = truncatedPath.padRight(maxTestNameLength);
+ io.stdout.write('\r ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}');
+ return success
+ ? MapEntry<String, CompileResult>(relativePath, CompileResult.success)
+ : MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure);
+ });
+ pendingResults.add(result);
+ }
}
final Map<String, CompileResult> results = Map<String, CompileResult>.fromEntries(await Future.wait(pendingResults));
stopwatch.stop();
@@ -121,8 +125,11 @@
final String resultsJson = const JsonEncoder.withIndent(' ').convert(<String, dynamic>{
'name': bundle.name,
'directory': bundle.testSet.directory,
- 'compiler': bundle.compileConfig.compiler.name,
- 'renderer': bundle.compileConfig.renderer.name,
+ 'builds': bundle.compileConfigs.map(
+ (CompileConfiguration config) => <String, dynamic>{
+ 'compiler': config.compiler.name,
+ 'renderer': config.renderer.name,
+ }).toList(),
'compileTimeInMs': stopwatch.elapsedMilliseconds,
'results': results.map((String k, CompileResult v) => MapEntry<String, String>(k, v.name)),
});
diff --git a/lib/web_ui/dev/steps/copy_artifacts_step.dart b/lib/web_ui/dev/steps/copy_artifacts_step.dart
index fa63d91..b02f6f4 100644
--- a/lib/web_ui/dev/steps/copy_artifacts_step.dart
+++ b/lib/web_ui/dev/steps/copy_artifacts_step.dart
@@ -33,7 +33,6 @@
@override
Future<void> run() async {
await environment.webTestsArtifactsDir.create(recursive: true);
- await copyTestBootstrapScripts();
await buildHostPage();
await copyTestFonts();
await copySkiaTestImages();
@@ -52,23 +51,6 @@
}
}
- Future<void> copyTestBootstrapScripts() async {
- for (final String filename in <String>[
- 'test_dart2js.js',
- 'test_dart2wasm.js',
- ]) {
- final io.File sourceFile = io.File(pathlib.join(
- environment.webUiDevDir.path,
- filename,
- ));
- final io.File targetFile = io.File(pathlib.join(
- environment.webTestsArtifactsDir.path,
- filename,
- ));
- await sourceFile.copy(targetFile.path);
- }
- }
-
Future<void> copyTestFonts() async {
const Map<String, String> testFonts = <String, String>{
'Ahem': 'ahem.ttf',
diff --git a/lib/web_ui/dev/steps/run_suite_step.dart b/lib/web_ui/dev/steps/run_suite_step.dart
index adb9a12..a19abdc 100644
--- a/lib/web_ui/dev/steps/run_suite_step.dart
+++ b/lib/web_ui/dev/steps/run_suite_step.dart
@@ -49,8 +49,6 @@
/// Require Skia Gold to be available and reachable.
final bool requireSkiaGold;
- bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
-
@override
String get description => 'run_suite';
@@ -65,7 +63,6 @@
_prepareTestResultsDirectory();
final BrowserEnvironment browserEnvironment = getBrowserEnvironment(
suite.runConfig.browser,
- enableWasmGC: isWasm,
useDwarf: useDwarf,
);
await browserEnvironment.prepare();
@@ -177,12 +174,18 @@
}
Future<SkiaGoldClient?> _createSkiaClient() async {
- final Renderer renderer = suite.testBundle.compileConfig.renderer;
+ if (suite.testBundle.compileConfigs.length > 1) {
+ // Multiple compile configs are only used for our fallback tests, which
+ // do not collect goldens.
+ return null;
+ }
+ final Renderer renderer = suite.testBundle.compileConfigs.first.renderer;
final CanvasKitVariant? variant = suite.runConfig.variant;
final io.Directory workDirectory = getSkiaGoldDirectoryForSuite(suite);
if (workDirectory.existsSync()) {
workDirectory.deleteSync(recursive: true);
}
+ final bool isWasm = suite.testBundle.compileConfigs.first.compiler == Compiler.dart2wasm;
final SkiaGoldClient skiaClient = SkiaGoldClient(
workDirectory,
dimensions: <String, String> {
diff --git a/lib/web_ui/dev/suite_filter.dart b/lib/web_ui/dev/suite_filter.dart
index 25b566a..91f2661 100644
--- a/lib/web_ui/dev/suite_filter.dart
+++ b/lib/web_ui/dev/suite_filter.dart
@@ -70,18 +70,28 @@
}
}
-class CompilerFilter extends AllowListSuiteFilter<Compiler> {
- CompilerFilter({required super.allowList});
+class CompilerFilter extends SuiteFilter {
+ CompilerFilter({required this.allowList});
+
+ final Set<Compiler> allowList;
@override
- Compiler getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.compiler;
+ SuiteFilterResult filterSuite(TestSuite suite) => suite.testBundle.compileConfigs.any(
+ (CompileConfiguration config) => allowList.contains(config.compiler)
+ ) ? SuiteFilterResult.accepted()
+ : SuiteFilterResult.rejected('Selected compilers not used in suite.');
}
-class RendererFilter extends AllowListSuiteFilter<Renderer> {
- RendererFilter({required super.allowList});
+class RendererFilter extends SuiteFilter {
+ RendererFilter({required this.allowList});
+
+ final Set<Renderer> allowList;
@override
- Renderer getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.renderer;
+ SuiteFilterResult filterSuite(TestSuite suite) => suite.testBundle.compileConfigs.any(
+ (CompileConfiguration config) => allowList.contains(config.renderer)
+ ) ? SuiteFilterResult.accepted()
+ : SuiteFilterResult.rejected('Selected renderers not used in suite.');
}
class CanvasKitVariantFilter extends AllowListSuiteFilter<CanvasKitVariant> {
diff --git a/lib/web_ui/dev/test_dart2js.js b/lib/web_ui/dev/test_dart2js.js
deleted file mode 100644
index 22f55b5..0000000
--- a/lib/web_ui/dev/test_dart2js.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// This script runs in HTML files and loads and instantiates unit tests
-// for the flutter web engine that are compiled to dart2js. It is based
-// off of the `test/dart.js` script from the `test` dart package.
-
-export const runTest = (configuration) => {
- // Sends an error message to the server indicating that the script failed to
- // load.
- //
- // This mimics a MultiChannel-formatted message.
- var sendLoadException = function (message) {
- window.parent.postMessage({
- "href": window.location.href,
- "data": [0, { "type": "loadException", "message": message }],
- "exception": true,
- }, window.location.origin);
- }
-
- // Listen for dartLoadException events and forward to the server.
- window.addEventListener('dartLoadException', function (e) {
- sendLoadException(e.detail);
- });
-
- // The basename of the current page.
- var name = window.location.href.replace(/.*\//, '').replace(/#.*/, '');
-
- // Find <link rel="x-dart-test">.
- var links = document.getElementsByTagName("link");
- var testLinks = [];
- var length = links.length;
- for (var i = 0; i < length; ++i) {
- if (links[i].rel == "x-dart-test") testLinks.push(links[i]);
- }
-
- if (testLinks.length != 1) {
- sendLoadException(
- 'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' +
- testLinks.length + '.');
- return;
- }
-
- var link = testLinks[0];
-
- if (link.href == '') {
- sendLoadException(
- 'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' +
- 'attribute.');
- return;
- }
-
- try {
- window._flutter.loader.loadEntrypoint({
- entrypointUrl: link.href + '.browser_test.dart.js',
- onEntrypointLoaded: function(engineInitializer) {
- engineInitializer.initializeEngine(configuration).then(function(appRunner) {
- appRunner.runApp();
- });
- }
- });
- } catch (exception) {
- const message = `Failed to bootstrap unit test: ${exception}`;
- sendLoadException(message);
- }
-};
diff --git a/lib/web_ui/dev/test_dart2wasm.js b/lib/web_ui/dev/test_dart2wasm.js
deleted file mode 100644
index e1d1c32..0000000
--- a/lib/web_ui/dev/test_dart2wasm.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// This script runs in HTML files and loads and instantiates dart unit tests
-// that are compiled to WebAssembly. It is based off of the `test/dart.js`
-// script from the `test` dart package.
-
-window.onload = async function () {
- // Sends an error message to the server indicating that the script failed to
- // load.
- //
- // This mimics a MultiChannel-formatted message.
- var sendLoadException = function (message) {
- window.parent.postMessage({
- "href": window.location.href,
- "data": [0, { "type": "loadException", "message": message }],
- "exception": true,
- }, window.location.origin);
- }
-
- // Listen for dartLoadException events and forward to the server.
- window.addEventListener('dartLoadException', function (e) {
- sendLoadException(e.detail);
- });
-
- // The basename of the current page.
- var name = window.location.href.replace(/.*\//, '').replace(/#.*/, '');
-
- // Find <link rel="x-dart-test">.
- var links = document.getElementsByTagName("link");
- var testLinks = [];
- var length = links.length;
- for (var i = 0; i < length; ++i) {
- if (links[i].rel == "x-dart-test") testLinks.push(links[i]);
- }
-
- if (testLinks.length != 1) {
- sendLoadException(
- 'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' +
- testLinks.length + '.');
- return;
- }
-
- var link = testLinks[0];
-
- if (link.href == '') {
- sendLoadException(
- 'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' +
- 'attribute.');
- return;
- }
-
- let dart2wasm_runtime;
- let moduleInstance;
- try {
- const isSkwasm = link.hasAttribute('skwasm');
- const imports = isSkwasm ? new Promise((resolve) => {
- const skwasmScript = document.createElement('script');
- skwasmScript.src = '/canvaskit/skwasm.js';
-
- document.body.appendChild(skwasmScript);
- skwasmScript.addEventListener('load', async () => {
- const skwasmInstance = await skwasm();
- window._flutter_skwasmInstance = skwasmInstance;
- resolve({
- "skwasm": skwasmInstance.wasmExports,
- "skwasmWrapper": skwasmInstance,
- "ffi": {
- "memory": skwasmInstance.wasmMemory,
- }
- });
- });
- }) : {};
-
- let baseName = link.href + '.browser_test.dart';
- dart2wasm_runtime = await import(baseName + '.mjs');
- const dartModulePromise = WebAssembly.compileStreaming(fetch(baseName + '.wasm'));
- moduleInstance = await dart2wasm_runtime.instantiate(dartModulePromise, imports);
- } catch (exception) {
- const message = `Failed to fetch and instantiate wasm module: ${exception}`;
- sendLoadException(message);
- }
-
- if (moduleInstance) {
- try {
- await dart2wasm_runtime.invoke(moduleInstance);
- } catch (exception) {
- const message = `Exception while invoking test: ${exception}`;
- sendLoadException(message);
- }
- }
-};
diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart
index 90d3172..b91c354 100644
--- a/lib/web_ui/dev/test_platform.dart
+++ b/lib/web_ui/dev/test_platform.dart
@@ -155,8 +155,9 @@
/// The URL for this server.
Uri get url => server.url.resolve('/');
- bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
- bool get needsCrossOriginIsolated => isWasm && suite.testBundle.compileConfig.renderer == Renderer.skwasm;
+ bool get needsCrossOriginIsolated => suite.testBundle.compileConfigs.any(
+ (CompileConfiguration config) => config.renderer == Renderer.skwasm
+ );
/// A [OneOffHandler] for servicing WebSocket connections for
/// [BrowserManager]s.
@@ -526,47 +527,58 @@
}
}
+ String _makeBuildConfigString(String scriptBase, CompileConfiguration config) {
+ return config.compiler == Compiler.dart2wasm ? '''
+ {
+ compileTarget: "${config.compiler.name}",
+ renderer: "${config.renderer.name}",
+ mainWasmPath: "$scriptBase.browser_test.dart.wasm",
+ jsSupportRuntimePath: "$scriptBase.browser_test.dart.mjs",
+ }
+''' : '''
+ {
+ compileTarget: "${config.compiler.name}",
+ renderer: "${config.renderer.name}",
+ mainJsPath: "$scriptBase.browser_test.dart.js",
+ }
+''';
+ }
+
/// Serves the HTML file that bootstraps the test.
shelf.Response _testBootstrapHandler(shelf.Request request) {
final String path = p.fromUri(request.url);
if (path.endsWith('.html')) {
final String test = '${p.withoutExtension(path)}.dart';
-
- final bool linkSkwasm = suite.testBundle.compileConfig.renderer == Renderer.skwasm;
- // Link to the Dart wrapper.
final String scriptBase = htmlEscape.convert(p.basename(test));
- final String link = '<link rel="x-dart-test" href="$scriptBase"${linkSkwasm ? " skwasm" : ""}>';
- final String bootstrapScript = isWasm ? '''
-<script>
- window.flutterConfiguration = {
- canvasKitBaseUrl: "/canvaskit/",
- // Some of our tests rely on color emoji
- useColorEmoji: true,
- canvasKitVariant: "${getCanvasKitVariant()}",
- };
-</script>
-<script src="/test_dart2wasm.js"></script>
- ''' : '''
+ final String buildConfigsString = suite.testBundle.compileConfigs.map(
+ (CompileConfiguration config) => _makeBuildConfigString(scriptBase, config)
+ ).join(',\n');
+ final String bootstrapScript = '''
<script src="/flutter_js/flutter.js"></script>
-<script type="module">
- import { runTest } from "/test_dart2js.js";
-
- runTest({
- canvasKitBaseUrl: "/canvaskit/",
- // Some of our tests rely on color emoji
- useColorEmoji: true,
- canvasKitVariant: "${getCanvasKitVariant()}",
+<script>
+ _flutter.buildConfig = {
+ builds: [
+ $buildConfigsString
+ ]
+ };
+ _flutter.loader.load({
+ config: {
+ canvasKitBaseUrl: "/canvaskit/",
+ // Some of our tests rely on color emoji
+ useColorEmoji: true,
+ canvasKitVariant: "${getCanvasKitVariant()}",
+ },
});
</script>
''';
+
return shelf.Response.ok('''
<!DOCTYPE html>
<html>
<head>
<meta name="assetBase" content="/">
- $link
$bootstrapScript
</head>
</html>
@@ -643,13 +655,16 @@
'debug': isDebug.toString()
});
+ final bool hasSourceMaps = suite.testBundle.compileConfigs.any(
+ (CompileConfiguration config) => config.compiler == Compiler.dart2js
+ );
final Future<BrowserManager?> future = BrowserManager.start(
browserEnvironment: browserEnvironment,
url: hostUrl,
future: completer.future,
packageConfig: packageConfig,
debug: isDebug,
- sourceMapDirectory: isWasm ? null : getBundleBuildDirectory(suite.testBundle),
+ sourceMapDirectory: hasSourceMaps ? getBundleBuildDirectory(suite.testBundle) : null,
);
// Store null values for browsers that error out so we know not to load them
diff --git a/lib/web_ui/flutter_js/BUILD.gn b/lib/web_ui/flutter_js/BUILD.gn
index c49893f..255cd2c 100644
--- a/lib/web_ui/flutter_js/BUILD.gn
+++ b/lib/web_ui/flutter_js/BUILD.gn
@@ -3,6 +3,7 @@
# found in the LICENSE file.
import("//flutter/build/esbuild/esbuild.gni")
+import("//flutter/shell/version/version.gni")
import("sources.gni")
group("flutter_js") {
diff --git a/lib/web_ui/flutter_js/sources.gni b/lib/web_ui/flutter_js/sources.gni
index ee00c55..11da02b 100644
--- a/lib/web_ui/flutter_js/sources.gni
+++ b/lib/web_ui/flutter_js/sources.gni
@@ -2,4 +2,17 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-flutter_js_source_list = [ "src/flutter.js" ]
+flutter_js_source_list = [
+ "src/base_uri.js",
+ "src/browser_environment.js",
+ "src/canvaskit_loader.js",
+ "src/entrypoint_loader.js",
+ "src/flutter.js",
+ "src/instantiate_wasm.js",
+ "src/loader.js",
+ "src/service_worker_loader.js",
+ "src/skwasm_loader.js",
+ "src/trusted_types.js",
+
+ "src/types.d.ts",
+]
diff --git a/lib/web_ui/flutter_js/src/base_uri.js b/lib/web_ui/flutter_js/src/base_uri.js
new file mode 100644
index 0000000..bfca8d3
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/base_uri.js
@@ -0,0 +1,17 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export const baseUri = ensureTrailingSlash(getBaseURI());
+
+function getBaseURI() {
+ const base = document.querySelector("base");
+ return (base && base.getAttribute("href")) || "";
+}
+
+function ensureTrailingSlash(uri) {
+ if (uri === "") {
+ return uri;
+ }
+ return uri.endsWith("/") ? uri : `${uri}/`;
+}
diff --git a/lib/web_ui/flutter_js/src/browser_environment.js b/lib/web_ui/flutter_js/src/browser_environment.js
new file mode 100644
index 0000000..64baabc
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/browser_environment.js
@@ -0,0 +1,46 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const isBlink = () => {
+ return (navigator.vendor === 'Google Inc.') ||
+ (navigator.agent === 'Edg/');
+}
+
+const hasImageCodecs = () => {
+ if (typeof ImageDecoder === 'undefined') {
+ return false;
+ }
+ // TODO(yjbanov): https://github.com/flutter/flutter/issues/122761
+ // Frequently, when a browser launches an API that other browsers already
+ // support, there are subtle incompatibilities that may cause apps to crash if,
+ // we blindly adopt the new implementation. This check prevents us from picking
+ // up potentially incompatible implementations of ImagdeDecoder API. Instead,
+ // when a new browser engine launches the API, we'll evaluate it and enable it
+ // explicitly.
+ return isBlink();
+}
+
+const hasChromiumBreakIterators = () => {
+ return (typeof Intl.v8BreakIterator !== "undefined") &&
+ (typeof Intl.Segmenter !== "undefined");
+}
+
+const supportsWasmGC = () => {
+ // This attempts to instantiate a wasm module that only will validate if the
+ // final WasmGC spec is implemented in the browser.
+ //
+ // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js
+ const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0];
+ return WebAssembly.validate(new Uint8Array(bytes));
+}
+
+/**
+ * @returns {import("./types").BrowserEnvironment}
+ */
+export const browserEnvironment = {
+ hasImageCodecs: hasImageCodecs(),
+ hasChromiumBreakIterators: hasChromiumBreakIterators(),
+ supportsWasmGC: supportsWasmGC(),
+ crossOriginIsolated: window.crossOriginIsolated,
+};
diff --git a/lib/web_ui/flutter_js/src/canvaskit_loader.js b/lib/web_ui/flutter_js/src/canvaskit_loader.js
new file mode 100644
index 0000000..cf88c3c
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/canvaskit_loader.js
@@ -0,0 +1,47 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import { createWasmInstantiator } from "./instantiate_wasm.js";
+
+export const loadCanvasKit = (deps, config, browserEnvironment, engineRevision) => {
+ if (window.flutterCanvasKit) {
+ // The user has set this global variable ahead of time, so we just return that.
+ return Promise.resolve(window.flutterCanvasKit);
+ }
+ window.flutterCanvasKitLoaded = new Promise((resolve, reject) => {
+ const supportsChromiumCanvasKit = browserEnvironment.hasChromiumBreakIterators && browserEnvironment.hasImageCodecs;
+ if (!supportsChromiumCanvasKit && config.canvasKitVariant == "chromium") {
+ throw "Chromium CanvasKit variant specifically requested, but unsupported in this browser";
+ }
+ const useChromiumCanvasKit = supportsChromiumCanvasKit && (config.canvasKitVariant !== "full");
+ let baseUrl = config.canvasKitBaseUrl ?? `https://www.gstatic.com/flutter-canvaskit/${engineRevision}/`;
+ if (useChromiumCanvasKit) {
+ baseUrl = `${baseUrl}/chromium/`;
+ }
+ let canvasKitUrl = `${baseUrl}canvaskit.js`;
+ if (deps.flutterTT.policy) {
+ canvasKitUrl = deps.flutterTT.policy.createScriptURL(canvasKitUrl);
+ }
+ const wasmInstantiator = createWasmInstantiator(`${baseUrl}canvaskit.wasm`);
+ const script = document.createElement("script");
+ script.src = canvasKitUrl;
+ if (config.nonce) {
+ script.nonce = config.nonce;
+ }
+ script.addEventListener('load', async () => {
+ try {
+ const canvasKit = await CanvasKitInit({
+ instantiateWasm: wasmInstantiator,
+ });
+ window.flutterCanvasKit = canvasKit;
+ resolve(canvasKit);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ script.addEventListener('error', reject);
+ document.head.appendChild(script);
+ });
+ return window.flutterCanvasKitLoaded;
+}
diff --git a/lib/web_ui/flutter_js/src/entrypoint_loader.js b/lib/web_ui/flutter_js/src/entrypoint_loader.js
new file mode 100644
index 0000000..db8b331
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/entrypoint_loader.js
@@ -0,0 +1,198 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import { baseUri } from "./base_uri.js";
+
+/**
+ * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
+ * the user when Flutter is ready, through `didCreateEngineInitializer`.
+ *
+ * @see https://docs.flutter.dev/development/platform-integration/web/initialization
+ */
+export class FlutterEntrypointLoader {
+ /**
+ * Creates a FlutterEntrypointLoader.
+ */
+ constructor() {
+ // Watchdog to prevent injecting the main entrypoint multiple times.
+ this._scriptLoaded = false;
+ }
+ /**
+ * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
+ * @param {TrustedTypesPolicy | undefined} policy
+ */
+ setTrustedTypesPolicy(policy) {
+ this._ttPolicy = policy;
+ }
+ /**
+ * @deprecated
+ * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
+ * user-specified `onEntrypointLoaded` callback with an EngineInitializer
+ * object when it's done.
+ *
+ * @param {*} options
+ * @returns {Promise | undefined} that will eventually resolve with an
+ * EngineInitializer, or will be rejected with the error caused by the loader.
+ * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
+ */
+ async loadEntrypoint(options) {
+ const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
+ options || {};
+ return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
+ }
+
+ /**
+ * Loads the entry point for a flutter application.
+ * @param {import("./types").ApplicationBuild} build
+ * Information about the specific build that is to be loaded
+ * @param {*} deps
+ * External dependencies that may be needed to load the app.
+ * @param {import("./types").FlutterConfiguration} config
+ * The application configuration. If no callback is specified, this will be
+ * passed along to engine when initializing it.
+ * @param {string} nonce
+ * A nonce to apply to the main application script tag, if necessary.
+ * @param {import("./types").OnEntrypointLoadedCallback?} onEntrypointLoaded
+ * An optional callback to invoke when the entrypoint is loaded. If no
+ * callback is supplied, the engine initializer and app runner will be
+ * automatically invoked on load, passing along the supplied flutter
+ * configuration.
+ */
+ async load(build, deps, config, nonce, onEntrypointLoaded) {
+ onEntrypointLoaded ??= (engineInitializer) => {
+ engineInitializer.initializeEngine(config).then((appRunner) => appRunner.runApp())
+ };
+ if (build.compileTarget === "dart2wasm") {
+ return this._loadWasmEntrypoint(build, deps, onEntrypointLoaded);
+ } else {
+ const mainPath = build.mainJsPath ?? "main.dart.js";
+ const entrypointUrl = `${baseUri}${mainPath}`;
+ return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
+ }
+ }
+
+ /**
+ * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
+ * function supplied by the user (if needed).
+ *
+ * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
+ * which is bound to the correct instance of the FlutterEntrypointLoader by
+ * the FlutterLoader object.
+ *
+ * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
+ */
+ didCreateEngineInitializer(engineInitializer) {
+ if (typeof this._didCreateEngineInitializerResolve === "function") {
+ this._didCreateEngineInitializerResolve(engineInitializer);
+ // Remove the resolver after the first time, so Flutter Web can hot restart.
+ this._didCreateEngineInitializerResolve = null;
+ // Make the engine revert to "auto" initialization on hot restart.
+ delete _flutter.loader.didCreateEngineInitializer;
+ }
+ if (typeof this._onEntrypointLoaded === "function") {
+ this._onEntrypointLoaded(engineInitializer);
+ }
+ }
+ /**
+ * Injects a script tag into the DOM, and configures this loader to be able to
+ * handle the "entrypoint loaded" notifications received from Flutter web.
+ *
+ * @param {string} entrypointUrl the URL of the script that will initialize
+ * Flutter.
+ * @param {Function} onEntrypointLoaded a callback that will be called when
+ * Flutter web notifies this object that the entrypoint is
+ * loaded.
+ * @returns {Promise | undefined} a Promise that resolves when the entrypoint
+ * is loaded, or undefined if `onEntrypointLoaded`
+ * is a function.
+ */
+ _loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
+ const useCallback = typeof onEntrypointLoaded === "function";
+ if (!this._scriptLoaded) {
+ this._scriptLoaded = true;
+ const scriptTag = this._createScriptTag(entrypointUrl, nonce);
+ if (useCallback) {
+ // Just inject the script tag, and return nothing; Flutter will call
+ // `didCreateEngineInitializer` when it's done.
+ console.debug("Injecting <script> tag. Using callback.");
+ this._onEntrypointLoaded = onEntrypointLoaded;
+ document.head.append(scriptTag);
+ } else {
+ // Inject the script tag and return a promise that will get resolved
+ // with the EngineInitializer object from Flutter when it calls
+ // `didCreateEngineInitializer` later.
+ return new Promise((resolve, reject) => {
+ console.debug(
+ "Injecting <script> tag. Using Promises. Use the callback approach instead!"
+ );
+ this._didCreateEngineInitializerResolve = resolve;
+ scriptTag.addEventListener("error", reject);
+ document.head.append(scriptTag);
+ });
+ }
+ }
+ }
+
+ /**
+ *
+ * @param {import("./types").WasmApplicationBuild} build
+ * @param {*} deps
+ * @param {import("./types").OnEntrypointLoadedCallback} onEntrypointLoaded
+ */
+ async _loadWasmEntrypoint(build, deps, onEntrypointLoaded) {
+ if (!this._scriptLoaded) {
+ this._scriptLoaded = true;
+
+ this._onEntrypointLoaded = onEntrypointLoaded;
+ const { mainWasmPath, jsSupportRuntimePath } = build;
+ const moduleUri = `${baseUri}${mainWasmPath}`;
+ let jsSupportRuntimeUri = `${baseUri}${jsSupportRuntimePath}`;
+ if (this._ttPolicy != null) {
+ jsSupportRuntimeUri = this._ttPolicy.createScriptURL(jsSupportRuntimeUri);
+ }
+ const dartModulePromise = WebAssembly.compileStreaming(fetch(moduleUri));
+
+ const jsSupportRuntime = await import(jsSupportRuntimeUri);
+
+ let imports;
+ if (build.renderer === "skwasm") {
+ imports = (async () => {
+ const skwasmInstance = await deps.skwasm;
+ window._flutter_skwasmInstance = skwasmInstance;
+ return {
+ skwasm: skwasmInstance.wasmExports,
+ skwasmWrapper: skwasmInstance,
+ ffi: {
+ memory: skwasmInstance.wasmMemory,
+ },
+ };
+ })();
+ } else {
+ imports = {};
+ }
+ const moduleInstance = await jsSupportRuntime.instantiate(dartModulePromise, imports);
+ await jsSupportRuntime.invoke(moduleInstance);
+ }
+ }
+
+ /**
+ * Creates a script tag for the given URL.
+ * @param {string} url
+ * @returns {HTMLScriptElement}
+ */
+ _createScriptTag(url, nonce) {
+ const scriptTag = document.createElement("script");
+ scriptTag.type = "application/javascript";
+ if (nonce) {
+ scriptTag.nonce = nonce;
+ }
+ // Apply TrustedTypes validation, if available.
+ let trustedUrl = url;
+ if (this._ttPolicy != null) {
+ trustedUrl = this._ttPolicy.createScriptURL(url);
+ }
+ scriptTag.src = trustedUrl;
+ return scriptTag;
+ }
+}
diff --git a/lib/web_ui/flutter_js/src/flutter.js b/lib/web_ui/flutter_js/src/flutter.js
index fcdc9c3..57208c7 100644
--- a/lib/web_ui/flutter_js/src/flutter.js
+++ b/lib/web_ui/flutter_js/src/flutter.js
@@ -2,380 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-var _flutter = window._flutter;
-if (!_flutter) {
- _flutter = window._flutter = {};
+import { FlutterLoader } from './loader.js';
+
+if (!window._flutter) {
+ window._flutter = {
+ loader: new FlutterLoader()
+ };
}
-_flutter.loader = null;
-
-(function () {
- "use strict";
-
- const baseUri = ensureTrailingSlash(getBaseURI());
-
- function getBaseURI() {
- const base = document.querySelector("base");
- return (base && base.getAttribute("href")) || "";
- }
-
- function ensureTrailingSlash(uri) {
- if (uri == "") {
- return uri;
- }
- return uri.endsWith("/") ? uri : `${uri}/`;
- }
-
- /**
- * Wraps `promise` in a timeout of the given `duration` in ms.
- *
- * Resolves/rejects with whatever the original `promises` does, or rejects
- * if `promise` takes longer to complete than `duration`. In that case,
- * `debugName` is used to compose a legible error message.
- *
- * If `duration` is < 0, the original `promise` is returned unchanged.
- * @param {Promise} promise
- * @param {number} duration
- * @param {string} debugName
- * @returns {Promise} a wrapped promise.
- */
- async function timeout(promise, duration, debugName) {
- if (duration < 0) {
- return promise;
- }
- let timeoutId;
- const _clock = new Promise((_, reject) => {
- timeoutId = setTimeout(() => {
- reject(
- new Error(
- `${debugName} took more than ${duration}ms to resolve. Moving on.`,
- {
- cause: timeout,
- }
- )
- );
- }, duration);
- });
-
- return Promise.race([promise, _clock]).finally(() => {
- clearTimeout(timeoutId);
- });
- }
-
- /**
- * Handles the creation of a TrustedTypes `policy` that validates URLs based
- * on an (optional) incoming array of RegExes.
- */
- class FlutterTrustedTypesPolicy {
- /**
- * Constructs the policy.
- * @param {[RegExp]} validPatterns the patterns to test URLs
- * @param {String} policyName the policy name (optional)
- */
- constructor(validPatterns, policyName = "flutter-js") {
- const patterns = validPatterns || [
- /\.js$/,
- ];
- if (window.trustedTypes) {
- this.policy = trustedTypes.createPolicy(policyName, {
- createScriptURL: function(url) {
- const parsed = new URL(url, window.location);
- const file = parsed.pathname.split("/").pop();
- const matches = patterns.some((pattern) => pattern.test(file));
- if (matches) {
- return parsed.toString();
- }
- console.error(
- "URL rejected by TrustedTypes policy",
- policyName, ":", url, "(download prevented)");
- }
- });
- }
- }
- }
-
- /**
- * Handles loading/reloading Flutter's service worker, if configured.
- *
- * @see: https://developers.google.com/web/fundamentals/primers/service-workers
- */
- class FlutterServiceWorkerLoader {
- /**
- * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
- * @param {TrustedTypesPolicy | undefined} policy
- */
- setTrustedTypesPolicy(policy) {
- this._ttPolicy = policy;
- }
-
- /**
- * Returns a Promise that resolves when the latest Flutter service worker,
- * configured by `settings` has been loaded and activated.
- *
- * Otherwise, the promise is rejected with an error message.
- * @param {*} settings Service worker settings
- * @returns {Promise} that resolves when the latest serviceWorker is ready.
- */
- loadServiceWorker(settings) {
- if (settings == null) {
- // In the future, settings = null -> uninstall service worker?
- console.debug("Null serviceWorker configuration. Skipping.");
- return Promise.resolve();
- }
- if (!("serviceWorker" in navigator)) {
- let errorMessage = "Service Worker API unavailable.";
- if (!window.isSecureContext) {
- errorMessage += "\nThe current context is NOT secure."
- errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts";
- }
- return Promise.reject(
- new Error(errorMessage)
- );
- }
- const {
- serviceWorkerVersion,
- serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`,
- timeoutMillis = 4000,
- } = settings;
-
- // Apply the TrustedTypes policy, if present.
- let url = serviceWorkerUrl;
- if (this._ttPolicy != null) {
- url = this._ttPolicy.createScriptURL(url);
- }
-
- const serviceWorkerActivation = navigator.serviceWorker
- .register(url)
- .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion))
- .then(this._waitForServiceWorkerActivation);
-
- // Timeout race promise
- return timeout(
- serviceWorkerActivation,
- timeoutMillis,
- "prepareServiceWorker"
- );
- }
-
- /**
- * Returns the latest service worker for the given `serviceWorkerRegistration`.
- *
- * This might return the current service worker, if there's no new service worker
- * awaiting to be installed/updated.
- *
- * @param {ServiceWorkerRegistration} serviceWorkerRegistration
- * @param {String} serviceWorkerVersion
- * @returns {Promise<ServiceWorker>}
- */
- async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) {
- if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) {
- // No active web worker and we have installed or are installing
- // one for the first time. Simply wait for it to activate.
- console.debug("Installing/Activating first service worker.");
- return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting;
- } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) {
- // When the app updates the serviceWorkerVersion changes, so we
- // need to ask the service worker to update.
- const newRegistration = await serviceWorkerRegistration.update();
- console.debug("Updating service worker.");
- return newRegistration.installing || newRegistration.waiting || newRegistration.active;
- } else {
- console.debug("Loading from existing service worker.");
- return serviceWorkerRegistration.active;
- }
- }
-
- /**
- * Returns a Promise that resolves when the `serviceWorker` changes its
- * state to "activated".
- *
- * @param {ServiceWorker} serviceWorker
- * @returns {Promise<void>}
- */
- async _waitForServiceWorkerActivation(serviceWorker) {
- if (!serviceWorker || serviceWorker.state == "activated") {
- if (!serviceWorker) {
- throw new Error("Cannot activate a null service worker!");
- } else {
- console.debug("Service worker already active.");
- return;
- }
- }
- return new Promise((resolve, _) => {
- serviceWorker.addEventListener("statechange", () => {
- if (serviceWorker.state == "activated") {
- console.debug("Activated new service worker.");
- resolve();
- }
- });
- });
- }
- }
-
- /**
- * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
- * the user when Flutter is ready, through `didCreateEngineInitializer`.
- *
- * @see https://docs.flutter.dev/development/platform-integration/web/initialization
- */
- class FlutterEntrypointLoader {
- /**
- * Creates a FlutterEntrypointLoader.
- */
- constructor() {
- // Watchdog to prevent injecting the main entrypoint multiple times.
- this._scriptLoaded = false;
- }
-
- /**
- * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
- * @param {TrustedTypesPolicy | undefined} policy
- */
- setTrustedTypesPolicy(policy) {
- this._ttPolicy = policy;
- }
-
- /**
- * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
- * user-specified `onEntrypointLoaded` callback with an EngineInitializer
- * object when it's done.
- *
- * @param {*} options
- * @returns {Promise | undefined} that will eventually resolve with an
- * EngineInitializer, or will be rejected with the error caused by the loader.
- * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
- */
- async loadEntrypoint(options) {
- const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
- options || {};
-
- return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
- }
-
- /**
- * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
- * function supplied by the user (if needed).
- *
- * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
- * which is bound to the correct instance of the FlutterEntrypointLoader by
- * the FlutterLoader object.
- *
- * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
- */
- didCreateEngineInitializer(engineInitializer) {
- if (typeof this._didCreateEngineInitializerResolve === "function") {
- this._didCreateEngineInitializerResolve(engineInitializer);
- // Remove the resolver after the first time, so Flutter Web can hot restart.
- this._didCreateEngineInitializerResolve = null;
- // Make the engine revert to "auto" initialization on hot restart.
- delete _flutter.loader.didCreateEngineInitializer;
- }
- if (typeof this._onEntrypointLoaded === "function") {
- this._onEntrypointLoaded(engineInitializer);
- }
- }
-
- /**
- * Injects a script tag into the DOM, and configures this loader to be able to
- * handle the "entrypoint loaded" notifications received from Flutter web.
- *
- * @param {string} entrypointUrl the URL of the script that will initialize
- * Flutter.
- * @param {Function} onEntrypointLoaded a callback that will be called when
- * Flutter web notifies this object that the entrypoint is
- * loaded.
- * @returns {Promise | undefined} a Promise that resolves when the entrypoint
- * is loaded, or undefined if `onEntrypointLoaded`
- * is a function.
- */
- _loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
- const useCallback = typeof onEntrypointLoaded === "function";
-
- if (!this._scriptLoaded) {
- this._scriptLoaded = true;
- const scriptTag = this._createScriptTag(entrypointUrl, nonce);
- if (useCallback) {
- // Just inject the script tag, and return nothing; Flutter will call
- // `didCreateEngineInitializer` when it's done.
- console.debug("Injecting <script> tag. Using callback.");
- this._onEntrypointLoaded = onEntrypointLoaded;
- document.body.append(scriptTag);
- } else {
- // Inject the script tag and return a promise that will get resolved
- // with the EngineInitializer object from Flutter when it calls
- // `didCreateEngineInitializer` later.
- return new Promise((resolve, reject) => {
- console.debug(
- "Injecting <script> tag. Using Promises. Use the callback approach instead!"
- );
- this._didCreateEngineInitializerResolve = resolve;
- scriptTag.addEventListener("error", reject);
- document.body.append(scriptTag);
- });
- }
- }
- }
-
- /**
- * Creates a script tag for the given URL.
- * @param {string} url
- * @returns {HTMLScriptElement}
- */
- _createScriptTag(url, nonce) {
- const scriptTag = document.createElement("script");
- scriptTag.type = "application/javascript";
- if (nonce) {
- scriptTag.nonce = nonce;
- }
- // Apply TrustedTypes validation, if available.
- let trustedUrl = url;
- if (this._ttPolicy != null) {
- trustedUrl = this._ttPolicy.createScriptURL(url);
- }
- scriptTag.src = trustedUrl;
- return scriptTag;
- }
- }
-
- /**
- * The public interface of _flutter.loader. Exposes two methods:
- * * loadEntrypoint (which coordinates the default Flutter web loading procedure)
- * * didCreateEngineInitializer (which is called by Flutter to notify that its
- * Engine is ready to be initialized)
- */
- class FlutterLoader {
- /**
- * Initializes the Flutter web app.
- * @param {*} options
- * @returns {Promise?} a (Deprecated) Promise that will eventually resolve
- * with an EngineInitializer, or will be rejected with
- * any error caused by the loader. Or Null, if the user
- * supplies an `onEntrypointLoaded` Function as an option.
- */
- async loadEntrypoint(options) {
- const { serviceWorker, ...entrypoint } = options || {};
-
- // A Trusted Types policy that is going to be used by the loader.
- const flutterTT = new FlutterTrustedTypesPolicy();
-
- // The FlutterServiceWorkerLoader instance could be injected as a dependency
- // (and dynamically imported from a module if not present).
- const serviceWorkerLoader = new FlutterServiceWorkerLoader();
- serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
- await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
- // Regardless of what happens with the injection of the SW, the show must go on
- console.warn("Exception while loading service worker:", e);
- });
-
- // The FlutterEntrypointLoader instance could be injected as a dependency
- // (and dynamically imported from a module if not present).
- const entrypointLoader = new FlutterEntrypointLoader();
- entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
- // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
- this.didCreateEngineInitializer =
- entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
- return entrypointLoader.loadEntrypoint(entrypoint);
- }
- }
-
- _flutter.loader = new FlutterLoader();
-})();
diff --git a/lib/web_ui/flutter_js/src/instantiate_wasm.js b/lib/web_ui/flutter_js/src/instantiate_wasm.js
new file mode 100644
index 0000000..29827f6
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/instantiate_wasm.js
@@ -0,0 +1,17 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a little helper function that helps us start the fetch and compilation
+// of an emscripten wasm module in parallel with the fetch of its script.
+export const createWasmInstantiator = (url) => {
+ const modulePromise = WebAssembly.compileStreaming(fetch(url));
+ return (imports, successCallback) => {
+ (async () => {
+ const module = await modulePromise;
+ const instance = await WebAssembly.instantiate(module, imports);
+ successCallback(instance, module);
+ })();
+ return {};
+ };
+}
diff --git a/lib/web_ui/flutter_js/src/loader.js b/lib/web_ui/flutter_js/src/loader.js
new file mode 100644
index 0000000..3d5fb4e
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/loader.js
@@ -0,0 +1,130 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import { browserEnvironment } from './browser_environment.js';
+import { FlutterEntrypointLoader } from './entrypoint_loader.js';
+import { FlutterServiceWorkerLoader } from './service_worker_loader.js';
+import { FlutterTrustedTypesPolicy } from './trusted_types.js';
+import { loadCanvasKit } from './canvaskit_loader.js';
+import { loadSkwasm } from './skwasm_loader.js';
+
+/**
+ * The public interface of _flutter.loader. Exposes two methods:
+ * * loadEntrypoint (which coordinates the default Flutter web loading procedure)
+ * * didCreateEngineInitializer (which is called by Flutter to notify that its
+ * Engine is ready to be initialized)
+ */
+export class FlutterLoader {
+ /**
+ * @deprecated Use `load` instead.
+ * Initializes the Flutter web app.
+ * @param {*} options
+ * @returns {Promise?} a (Deprecated) Promise that will eventually resolve
+ * with an EngineInitializer, or will be rejected with
+ * any error caused by the loader. Or Null, if the user
+ * supplies an `onEntrypointLoaded` Function as an option.
+ */
+ async loadEntrypoint(options) {
+ const { serviceWorker, ...entrypoint } = options || {};
+ // A Trusted Types policy that is going to be used by the loader.
+ const flutterTT = new FlutterTrustedTypesPolicy();
+ // The FlutterServiceWorkerLoader instance could be injected as a dependency
+ // (and dynamically imported from a module if not present).
+ const serviceWorkerLoader = new FlutterServiceWorkerLoader();
+ serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
+ await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
+ // Regardless of what happens with the injection of the SW, the show must go on
+ console.warn("Exception while loading service worker:", e);
+ });
+ // The FlutterEntrypointLoader instance could be injected as a dependency
+ // (and dynamically imported from a module if not present).
+ const entrypointLoader = new FlutterEntrypointLoader();
+ entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
+ // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
+ this.didCreateEngineInitializer =
+ entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
+ return entrypointLoader.loadEntrypoint(entrypoint);
+ }
+
+ /**
+ * Loads and initializes a flutter application.
+ * @param {Object} options
+ * @param {import("/.types".ServiceWorkerSettings?)} options.serviceWorkerSettings
+ * Settings for the service worker to be loaded. Can pass `undefined` or
+ * `null` to not launch a service worker at all.
+ * @param {import("/.types".OnEntryPointLoadedCallback)} options.onEntrypointLoaded
+ * An optional callback to invoke
+ * @param {string} options.nonce
+ * A nonce to be applied to the main JS script when loading it, which may
+ * be required by the sites Content-Security-Policy.
+ * For more details, see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src here}.
+ * @param {import("./types".FlutterConfiguration)} arg.config
+ */
+ async load({
+ serviceWorkerSettings,
+ onEntrypointLoaded,
+ nonce,
+ config,
+ } = {}) {
+ config ??= {};
+
+ /** @type {import("./types").BuildConfig} */
+ const buildConfig = _flutter.buildConfig;
+ if (!buildConfig) {
+ throw "FlutterLoader.load requires _flutter.buildConfig to be set";
+ }
+
+ const rendererIsCompatible = (renderer) => {
+ switch (renderer) {
+ case "skwasm":
+ return browserEnvironment.crossOriginIsolated
+ && browserEnvironment.hasChromiumBreakIterators
+ && browserEnvironment.hasImageCodecs
+ && browserEnvironment.supportsWasmGC;
+ default:
+ return true;
+ }
+ }
+
+ const buildIsCompatible = (build) => {
+ if (build.compileTarget === "dart2wasm" && !browserEnvironment.supportsWasmGC) {
+ return false;
+ }
+ if (config.renderer && config.renderer != build.renderer) {
+ return false;
+ }
+ return rendererIsCompatible(build.renderer);
+ };
+ const build = buildConfig.builds.find(buildIsCompatible);
+ if (!build) {
+ throw "FlutterLoader could not find a build compatible with configuration and environment.";
+ }
+
+ const deps = {};
+ deps.flutterTT = new FlutterTrustedTypesPolicy();
+ if (serviceWorkerSettings) {
+ deps.serviceWorkerLoader = new FlutterServiceWorkerLoader();
+ deps.serviceWorkerLoader.setTrustedTypesPolicy(deps.flutterTT.policy);
+ await deps.serviceWorkerLoader.loadServiceWorker(serviceWorkerSettings).catch(e => {
+ // Regardless of what happens with the injection of the SW, the show must go on
+ console.warn("Exception while loading service worker:", e);
+ });
+ }
+
+ if (build.renderer === "canvaskit") {
+ deps.canvasKit = loadCanvasKit(deps, config, browserEnvironment, buildConfig.engineRevision);
+ } else if (build.renderer === "skwasm") {
+ deps.skwasm = loadSkwasm(deps, config, browserEnvironment, buildConfig.engineRevision);
+ }
+
+ // The FlutterEntrypointLoader instance could be injected as a dependency
+ // (and dynamically imported from a module if not present).
+ const entrypointLoader = new FlutterEntrypointLoader();
+ entrypointLoader.setTrustedTypesPolicy(deps.flutterTT.policy);
+ // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
+ this.didCreateEngineInitializer =
+ entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
+ return entrypointLoader.load(build, deps, config, nonce, onEntrypointLoaded);
+ }
+}
diff --git a/lib/web_ui/flutter_js/src/service_worker_loader.js b/lib/web_ui/flutter_js/src/service_worker_loader.js
new file mode 100644
index 0000000..7c4ff3e
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/service_worker_loader.js
@@ -0,0 +1,152 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import { baseUri } from "./base_uri.js";
+
+/**
+ * Wraps `promise` in a timeout of the given `duration` in ms.
+ *
+ * Resolves/rejects with whatever the original `promises` does, or rejects
+ * if `promise` takes longer to complete than `duration`. In that case,
+ * `debugName` is used to compose a legible error message.
+ *
+ * If `duration` is < 0, the original `promise` is returned unchanged.
+ * @param {Promise} promise
+ * @param {number} duration
+ * @param {string} debugName
+ * @returns {Promise} a wrapped promise.
+ */
+async function timeout(promise, duration, debugName) {
+ if (duration < 0) {
+ return promise;
+ }
+ let timeoutId;
+ const _clock = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ reject(
+ new Error(
+ `${debugName} took more than ${duration}ms to resolve. Moving on.`,
+ {
+ cause: timeout,
+ }
+ )
+ );
+ }, duration);
+ });
+ return Promise.race([promise, _clock]).finally(() => {
+ clearTimeout(timeoutId);
+ });
+}
+
+/**
+ * Handles loading/reloading Flutter's service worker, if configured.
+ *
+ * @see: https://developers.google.com/web/fundamentals/primers/service-workers
+ */
+export class FlutterServiceWorkerLoader {
+ /**
+ * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
+ * @param {TrustedTypesPolicy | undefined} policy
+ */
+ setTrustedTypesPolicy(policy) {
+ this._ttPolicy = policy;
+ }
+ /**
+ * Returns a Promise that resolves when the latest Flutter service worker,
+ * configured by `settings` has been loaded and activated.
+ *
+ * Otherwise, the promise is rejected with an error message.
+ * @param {import("./types").ServiceWorkerSettings} settings Service worker settings
+ * @returns {Promise} that resolves when the latest serviceWorker is ready.
+ */
+ loadServiceWorker(settings) {
+ if (!settings) {
+ // In the future, settings = null -> uninstall service worker?
+ console.debug("Null serviceWorker configuration. Skipping.");
+ return Promise.resolve();
+ }
+ if (!("serviceWorker" in navigator)) {
+ let errorMessage = "Service Worker API unavailable.";
+ if (!window.isSecureContext) {
+ errorMessage += "\nThe current context is NOT secure."
+ errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts";
+ }
+ return Promise.reject(
+ new Error(errorMessage)
+ );
+ }
+ const {
+ serviceWorkerVersion,
+ serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`,
+ timeoutMillis = 4000,
+ } = settings;
+ // Apply the TrustedTypes policy, if present.
+ let url = serviceWorkerUrl;
+ if (this._ttPolicy != null) {
+ url = this._ttPolicy.createScriptURL(url);
+ }
+ const serviceWorkerActivation = navigator.serviceWorker
+ .register(url)
+ .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion))
+ .then(this._waitForServiceWorkerActivation);
+ // Timeout race promise
+ return timeout(
+ serviceWorkerActivation,
+ timeoutMillis,
+ "prepareServiceWorker"
+ );
+ }
+ /**
+ * Returns the latest service worker for the given `serviceWorkerRegistration`.
+ *
+ * This might return the current service worker, if there's no new service worker
+ * awaiting to be installed/updated.
+ *
+ * @param {ServiceWorkerRegistration} serviceWorkerRegistration
+ * @param {string} serviceWorkerVersion
+ * @returns {Promise<ServiceWorker>}
+ */
+ async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) {
+ if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) {
+ // No active web worker and we have installed or are installing
+ // one for the first time. Simply wait for it to activate.
+ console.debug("Installing/Activating first service worker.");
+ return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting;
+ } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) {
+ // When the app updates the serviceWorkerVersion changes, so we
+ // need to ask the service worker to update.
+ const newRegistration = await serviceWorkerRegistration.update();
+ console.debug("Updating service worker.");
+ return newRegistration.installing || newRegistration.waiting || newRegistration.active;
+ } else {
+ console.debug("Loading from existing service worker.");
+ return serviceWorkerRegistration.active;
+ }
+ }
+ /**
+ * Returns a Promise that resolves when the `serviceWorker` changes its
+ * state to "activated".
+ *
+ * @param {ServiceWorker} serviceWorker
+ * @returns {Promise<void>}
+ */
+ async _waitForServiceWorkerActivation(serviceWorker) {
+ if (!serviceWorker || serviceWorker.state === "activated") {
+ if (!serviceWorker) {
+ throw new Error("Cannot activate a null service worker!");
+ } else {
+ console.debug("Service worker already active.");
+ return;
+ }
+ }
+ return new Promise((resolve, _) => {
+ serviceWorker.addEventListener("statechange", () => {
+ if (serviceWorker.state === "activated") {
+ console.debug("Activated new service worker.");
+ resolve();
+ }
+ });
+ });
+ }
+}
diff --git a/lib/web_ui/flutter_js/src/skwasm_loader.js b/lib/web_ui/flutter_js/src/skwasm_loader.js
new file mode 100644
index 0000000..86e1b52
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/skwasm_loader.js
@@ -0,0 +1,47 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import { createWasmInstantiator } from "./instantiate_wasm.js";
+
+export const loadSkwasm = (deps, config, browserEnvironment, engineRevision) => {
+ return new Promise((resolve, reject) => {
+ const baseUrl = config.canvasKitBaseUrl ?? `https://www.gstatic.com/flutter-canvaskit/${engineRevision}/`;
+ let skwasmUrl = `${baseUrl}skwasm.js`;
+ if (deps.flutterTT.policy) {
+ skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
+ }
+ const wasmInstantiator = createWasmInstantiator(`${baseUrl}skwasm.wasm`);
+ const script = document.createElement("script");
+ script.src = skwasmUrl;
+ if (config.nonce) {
+ script.nonce = config.nonce;
+ }
+ script.addEventListener('load', async () => {
+ try {
+ const skwasmInstance = await skwasm({
+ instantiateWasm: wasmInstantiator,
+ locateFile: (fileName, scriptDirectory) => {
+ // When hosted via a CDN or some other url that is not the same
+ // origin as the main script of the page, we will fail to create
+ // a web worker with the .worker.js script. This workaround will
+ // make sure that the worker JS can be loaded regardless of where
+ // it is hosted.
+ const url = scriptDirectory + fileName;
+ if (url.endsWith('.worker.js')) {
+ return URL.createObjectURL(new Blob(
+ [`importScripts('${url}');`],
+ { 'type': 'application/javascript' }));
+ }
+ return url;
+ }
+ });
+ resolve(skwasmInstance);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ script.addEventListener('error', reject);
+ document.head.appendChild(script);
+ });
+}
diff --git a/lib/web_ui/flutter_js/src/trusted_types.js b/lib/web_ui/flutter_js/src/trusted_types.js
new file mode 100644
index 0000000..76db604
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/trusted_types.js
@@ -0,0 +1,36 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Handles the creation of a TrustedTypes `policy` that validates URLs based
+ * on an (optional) incoming array of RegExes.
+ */
+export class FlutterTrustedTypesPolicy {
+ /**
+ * Constructs the policy.
+ * @param {[RegExp]} validPatterns the patterns to test URLs
+ * @param {String} policyName the policy name (optional)
+ */
+ constructor(validPatterns, policyName = "flutter-js") {
+ const patterns = validPatterns || [
+ /\.js$/,
+ /\.mjs$/,
+ ];
+ if (window.trustedTypes) {
+ this.policy = trustedTypes.createPolicy(policyName, {
+ createScriptURL: function (url) {
+ const parsed = new URL(url, window.location);
+ const file = parsed.pathname.split("/").pop();
+ const matches = patterns.some((pattern) => pattern.test(file));
+ if (matches) {
+ return parsed.toString();
+ }
+ console.error(
+ "URL rejected by TrustedTypes policy",
+ policyName, ":", url, "(download prevented)");
+ }
+ });
+ }
+ }
+}
diff --git a/lib/web_ui/flutter_js/src/types.d.ts b/lib/web_ui/flutter_js/src/types.d.ts
new file mode 100644
index 0000000..9cd1f7a
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/types.d.ts
@@ -0,0 +1,73 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+type JSCompileTarget = "dart2js" | "dartdevc";
+type WasmCompileTarget = "dart2wasm";
+
+export type CompileTarget = JSCompileTarget | WasmCompileTarget;
+
+export type WebRenderer =
+ "html" |
+ "canvaskit" |
+ "skwasm";
+
+interface ApplicationBuildBase {
+ renderer: WebRenderer;
+}
+
+export interface JSApplicationBuild extends ApplicationBuildBase {
+ compileTarget: JSCompileTarget;
+ mainJsPath: string;
+}
+
+export interface WasmApplicationBuild extends ApplicationBuildBase {
+ compileTarget: WasmCompileTarget;
+ mainWasmPath: string;
+ jsSupportRuntimePath: string;
+}
+
+export type ApplicationBuild = JSApplicationBuild | WasmApplicationBuild;
+
+export interface BuildConfig {
+ serviceWorkerVersion: string;
+ engineRevision: string;
+ builds: ApplicationBuild[];
+}
+
+export interface BrowserEnvironment {
+ hasImageCodecs: boolean;
+ hasChromiumBreakIterators: boolean;
+ supportsWasmGC: boolean;
+ crossOriginIsolated: boolean;
+}
+
+type CanvasKitVariant =
+ "auto" |
+ "full" |
+ "chromium";
+
+export interface FlutterConfiguration {
+ assetBase: string?;
+ canvasKitBaseUrl: string?;
+ canvasKitVariant: CanvasKitVariant?;
+ renderer: WebRenderer?;
+ hostElement: HtmlElement?;
+}
+
+export interface ServiceWorkerSettings {
+ serviceWorkerVersion: string;
+ serviceWorkerUrl: string?;
+ timeoutMillis: number?;
+}
+
+export interface AppRunner {
+ runApp: () => void;
+}
+
+export interface EngineInitializer {
+ initializeEngine: () => Promise<AppRunner>;
+}
+
+export type OnEntrypointLoadedCallback =
+ (initializer: EngineInitializer) => void;
diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
index a0de371..39608d2 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
@@ -43,6 +43,9 @@
@JS('window.flutterCanvasKit')
external CanvasKit? get windowFlutterCanvasKit;
+@JS('window.flutterCanvasKitLoaded')
+external JSPromise<JSAny>? get windowFlutterCanvasKitLoaded;
+
@JS()
@anonymous
@staticInterop
diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
index deb63b4..b3a8b3b 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
@@ -65,6 +65,9 @@
_initialized ??= () async {
if (windowFlutterCanvasKit != null) {
canvasKit = windowFlutterCanvasKit!;
+ } else if (windowFlutterCanvasKitLoaded != null) {
+ // CanvasKit is being preloaded by flutter.js. Wait for it to complete.
+ canvasKit = await promiseToFuture<CanvasKit>(windowFlutterCanvasKitLoaded!);
} else {
canvasKit = await downloadCanvasKit();
windowFlutterCanvasKit = canvasKit;
diff --git a/lib/web_ui/test/canvaskit/hot_restart_test.dart b/lib/web_ui/test/canvaskit/hot_restart_test.dart
index b032f53..3ee6903 100644
--- a/lib/web_ui/test/canvaskit/hot_restart_test.dart
+++ b/lib/web_ui/test/canvaskit/hot_restart_test.dart
@@ -12,8 +12,6 @@
void testMain() {
test('CanvasKit reuses the instance already set on `window`', () async {
- expect(windowFlutterCanvasKit, isNull);
-
// First initialization should make CanvasKit available through `window`.
await renderer.initialize();
expect(windowFlutterCanvasKit, isNotNull);
diff --git a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
index 0cfd55a..709b88a 100644
--- a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
+++ b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
@@ -16,7 +16,6 @@
test('services are initalized separately from UI', () async {
final JsFlutterConfiguration? config = await bootstrapAndExtractConfig();
expect(scheduleFrameCallback, isNull);
- expect(windowFlutterCanvasKit, isNull);
expect(findGlassPane(), isNull);
expect(RawKeyboard.instance, isNull);
diff --git a/lib/web_ui/test/fallbacks/fallbacks_test.dart b/lib/web_ui/test/fallbacks/fallbacks_test.dart
new file mode 100644
index 0000000..da9ac9a
--- /dev/null
+++ b/lib/web_ui/test/fallbacks/fallbacks_test.dart
@@ -0,0 +1,31 @@
+// Copyright 2013 The Flutter Authors. 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:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+
+import '../common/test_initialization.dart';
+import '../ui/utils.dart';
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+Future<void> testMain() async {
+ setUpUnitTests(
+ setUpTestViewDimensions: false,
+ );
+
+ test('bootstrapper selects correct builds', () {
+ if (browserEngine == BrowserEngine.blink) {
+ expect(isWasm, isTrue);
+ expect(isSkwasm, isTrue);
+ } else {
+ expect(isWasm, isFalse);
+ expect(isCanvasKit, isTrue);
+ }
+ });
+}
diff --git a/lib/web_ui/test/felt_config.yaml b/lib/web_ui/test/felt_config.yaml
index 6cc145b..2e20a35 100644
--- a/lib/web_ui/test/felt_config.yaml
+++ b/lib/web_ui/test/felt_config.yaml
@@ -42,59 +42,60 @@
- name: ui
directory: ui
- # This just has a single test that makes sure the skwasm stub renderer is
- # included when compiling to JS
- - name: skwasm_stub
- directory: skwasm_stub
+ # Tests for fallback functionality between build variants
+ - name: fallbacks
+ directory: fallbacks
test-bundles:
- name: dart2js-html-engine
test-set: engine
- compile-config: dart2js-html
+ compile-configs: dart2js-html
- name: dart2js-html-html
test-set: html
- compile-config: dart2js-html
+ compile-configs: dart2js-html
- name: dart2js-html-ui
test-set: ui
- compile-config: dart2js-html
+ compile-configs: dart2js-html
- name: dart2js-canvaskit-canvaskit
test-set: canvaskit
- compile-config: dart2js-canvaskit
+ compile-configs: dart2js-canvaskit
- name: dart2js-canvaskit-ui
test-set: ui
- compile-config: dart2js-canvaskit
-
- - name: dart2js-skwasm-skwasm_stub
- test-set: skwasm_stub
- compile-config: dart2js-skwasm
+ compile-configs: dart2js-canvaskit
- name: dart2wasm-html-engine
test-set: engine
- compile-config: dart2wasm-html
+ compile-configs: dart2wasm-html
- name: dart2wasm-html-html
test-set: html
- compile-config: dart2wasm-html
+ compile-configs: dart2wasm-html
- name: dart2wasm-html-ui
test-set: ui
- compile-config: dart2wasm-html
+ compile-configs: dart2wasm-html
- name: dart2wasm-canvaskit-canvaskit
test-set: canvaskit
- compile-config: dart2wasm-canvaskit
+ compile-configs: dart2wasm-canvaskit
- name: dart2wasm-canvaskit-ui
test-set: ui
- compile-config: dart2wasm-canvaskit
+ compile-configs: dart2wasm-canvaskit
- name: dart2wasm-skwasm-ui
test-set: ui
- compile-config: dart2wasm-skwasm
+ compile-configs: dart2wasm-skwasm
+
+ - name: fallbacks
+ test-set: fallbacks
+ compile-configs:
+ - dart2wasm-skwasm
+ - dart2js-canvaskit
run-configs:
- name: chrome
@@ -144,10 +145,6 @@
run-config: chrome
artifact-deps: [ canvaskit_chromium ]
- - name: chrome-dart2js-skwasm-skwasm_stub
- test-bundle: dart2js-skwasm-skwasm_stub
- run-config: chrome
-
- name: chrome-full-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: chrome-full
@@ -270,3 +267,18 @@
test-bundle: dart2wasm-canvaskit-ui
run-config: chrome-full
artifact-deps: [ canvaskit ]
+
+ - name: chrome-fallbacks
+ test-bundle: fallbacks
+ run-config: chrome
+ artifact-deps: [ canvaskit, skwasm ]
+
+ - name: firefox-fallbacks
+ test-bundle: fallbacks
+ run-config: firefox
+ artifact-deps: [ canvaskit ]
+
+ - name: safari-fallbacks
+ test-bundle: fallbacks
+ run-config: safari
+ artifact-deps: [ canvaskit ]
diff --git a/lib/web_ui/test/skwasm_stub/smoke_test.dart b/lib/web_ui/test/skwasm_stub/smoke_test.dart
deleted file mode 100644
index 7361d9c..0000000
--- a/lib/web_ui/test/skwasm_stub/smoke_test.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-@TestOn('chrome || safari || firefox')
-library;
-
-import 'dart:async';
-
-import 'package:test/bootstrap/browser.dart';
-import 'package:test/test.dart';
-import 'package:ui/src/engine/renderer.dart';
-import 'package:ui/src/engine/skwasm/skwasm_stub/renderer.dart';
-
-void main() {
- internalBootstrapBrowserTest(() => testMain);
-}
-
-Future<void> testMain() async {
- group('Skwasm stub tests', () {
- test('Skwasm stub renderer throws', () {
- expect(renderer, isA<SkwasmRenderer>());
- expect(() {
- renderer.initialize();
- }, throwsUnimplementedError);
- });
- });
-}