From 13d2c536877f509fbc65d7a74255fea71151054a Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 10 Jul 2024 09:19:28 -0700 Subject: [PATCH] Reland (x2) "Output .js files as ES6 modules. (flutter#52023)" (#53718) Second attempt to reland https://github.com/flutter/engine/pull/52023 Fixes since the previous reland attempt: * We need to pass the skwasm main JS URI when loading the module so that it can pass that along to the worker. Since the worker uses the workaround to allow a cross script worker, it has trouble locating the main JS URI in relation to itself in a way that actually works for dynamic imports, so passing it along fixes that issue. * Some of the Google3 tests relied on the relative default canvaskit path. Dynamic module imports seems to not handle relative paths the way we expect, so we do our own URL resolution using the URL constructor before passing it into the dynamic import API. Also cleaned up some of the other relative pathing stuff that we do around the base URI. in flutter.js --- DEPS | 2 +- lib/web_ui/dev/test_platform.dart | 1 + lib/web_ui/flutter_js/src/canvaskit_loader.js | 41 ++++-------- .../flutter_js/src/entrypoint_loader.js | 10 +-- .../flutter_js/src/service_worker_loader.js | 4 +- lib/web_ui/flutter_js/src/skwasm_loader.js | 66 ++++++++----------- lib/web_ui/flutter_js/src/utils.js | 11 ++-- .../src/engine/canvaskit/canvaskit_api.dart | 60 ++++++----------- lib/web_ui/lib/src/engine/dom.dart | 19 ++++-- .../does_not_mock_module_exports_test.dart | 7 -- .../initialization/services_vs_ui_test.dart | 10 +-- .../test/engine/configuration_test.dart | 12 ++-- 12 files changed, 97 insertions(+), 146 deletions(-) diff --git a/DEPS b/DEPS index 459c0c6bb3f42..a8137624ab0bc 100644 --- a/DEPS +++ b/DEPS @@ -277,7 +277,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + '8c2d66fa4e6298894425f5bdd0591bc5b1154c53', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'e265c359126b24351f534080fb22edaa159f2215', 'src/flutter/third_party/depot_tools': Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '580b4ff3f5cd0dcaa2eacda28cefe0f45320e8f7', diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 34954329d30f7..dd871b16ddcaf 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -575,6 +575,7 @@ class BrowserPlatform extends PlatformPlugin { // Some of our tests rely on color emoji useColorEmoji: true, canvasKitVariant: "${getCanvasKitVariant()}", + canvasKitBaseUrl: "/canvaskit", }, }); diff --git a/lib/web_ui/flutter_js/src/canvaskit_loader.js b/lib/web_ui/flutter_js/src/canvaskit_loader.js index 64dc6734ec273..eb79fa24b8a52 100644 --- a/lib/web_ui/flutter_js/src/canvaskit_loader.js +++ b/lib/web_ui/flutter_js/src/canvaskit_loader.js @@ -3,14 +3,14 @@ // found in the LICENSE file. import { createWasmInstantiator } from "./instantiate_wasm.js"; -import { joinPathSegments } from "./utils.js"; +import { resolveUrlWithSegments } from "./utils.js"; export const loadCanvasKit = (deps, config, browserEnvironment, canvasKitBaseUrl) => { - 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) => { + window.flutterCanvasKitLoaded = (async () => { + if (window.flutterCanvasKit) { + // The user has set this global variable ahead of time, so we just return that. + return window.flutterCanvasKit; + } const supportsChromiumCanvasKit = browserEnvironment.hasChromiumBreakIterators && browserEnvironment.hasImageCodecs; if (!supportsChromiumCanvasKit && config.canvasKitVariant == "chromium") { throw "Chromium CanvasKit variant specifically requested, but unsupported in this browser"; @@ -18,31 +18,18 @@ export const loadCanvasKit = (deps, config, browserEnvironment, canvasKitBaseUrl const useChromiumCanvasKit = supportsChromiumCanvasKit && (config.canvasKitVariant !== "full"); let baseUrl = canvasKitBaseUrl; if (useChromiumCanvasKit) { - baseUrl = joinPathSegments(baseUrl, "chromium"); + baseUrl = resolveUrlWithSegments(baseUrl, "chromium"); } - let canvasKitUrl = joinPathSegments(baseUrl, "canvaskit.js"); + let canvasKitUrl = resolveUrlWithSegments(baseUrl, "canvaskit.js"); if (deps.flutterTT.policy) { canvasKitUrl = deps.flutterTT.policy.createScriptURL(canvasKitUrl); } - const wasmInstantiator = createWasmInstantiator(joinPathSegments(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); - } + const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, "canvaskit.wasm")); + const canvasKitModule = await import(canvasKitUrl); + window.flutterCanvasKit = await canvasKitModule.default({ + instantiateWasm: wasmInstantiator, }); - script.addEventListener("error", reject); - document.head.appendChild(script); - }); + return window.flutterCanvasKit; + })(); 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 index 3e789c05e718b..dfc769d12a8af 100644 --- a/lib/web_ui/flutter_js/src/entrypoint_loader.js +++ b/lib/web_ui/flutter_js/src/entrypoint_loader.js @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { baseUri, joinPathSegments } from "./utils.js"; +import { resolveUrlWithSegments } from "./utils.js"; /** * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying @@ -37,7 +37,7 @@ export class FlutterEntrypointLoader { * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`. */ async loadEntrypoint(options) { - const { entrypointUrl = joinPathSegments(baseUri, "main.dart.js"), onEntrypointLoaded, nonce } = + const { entrypointUrl = resolveUrlWithSegments("main.dart.js"), onEntrypointLoaded, nonce } = options || {}; return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce); } @@ -68,7 +68,7 @@ export class FlutterEntrypointLoader { return this._loadWasmEntrypoint(build, deps, entryPointBaseUrl, onEntrypointLoaded); } else { const mainPath = build.mainJsPath ?? "main.dart.js"; - const entrypointUrl = joinPathSegments(baseUri, entryPointBaseUrl, mainPath); + const entrypointUrl = resolveUrlWithSegments(entryPointBaseUrl, mainPath); return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce); } } @@ -148,8 +148,8 @@ export class FlutterEntrypointLoader { this._onEntrypointLoaded = onEntrypointLoaded; const { mainWasmPath, jsSupportRuntimePath } = build; - const moduleUri = joinPathSegments(baseUri, entrypointBaseUrl, mainWasmPath); - let jsSupportRuntimeUri = joinPathSegments(baseUri, entrypointBaseUrl, jsSupportRuntimePath); + const moduleUri = resolveUrlWithSegments(entrypointBaseUrl, mainWasmPath); + let jsSupportRuntimeUri = resolveUrlWithSegments(entrypointBaseUrl, jsSupportRuntimePath); if (this._ttPolicy != null) { jsSupportRuntimeUri = this._ttPolicy.createScriptURL(jsSupportRuntimeUri); } diff --git a/lib/web_ui/flutter_js/src/service_worker_loader.js b/lib/web_ui/flutter_js/src/service_worker_loader.js index 20d9f25f68728..15302f53eb64d 100644 --- a/lib/web_ui/flutter_js/src/service_worker_loader.js +++ b/lib/web_ui/flutter_js/src/service_worker_loader.js @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { baseUri, joinPathSegments } from "./utils.js"; +import { resolveUrlWithSegments } from "./utils.js"; /** * Wraps `promise` in a timeout of the given `duration` in ms. @@ -78,7 +78,7 @@ export class FlutterServiceWorkerLoader { } const { serviceWorkerVersion, - serviceWorkerUrl = joinPathSegments(baseUri, `flutter_service_worker.js?v=${serviceWorkerVersion}`), + serviceWorkerUrl = resolveUrlWithSegments(`flutter_service_worker.js?v=${serviceWorkerVersion}`), timeoutMillis = 4000, } = settings; // Apply the TrustedTypes policy, if present. diff --git a/lib/web_ui/flutter_js/src/skwasm_loader.js b/lib/web_ui/flutter_js/src/skwasm_loader.js index 241f4e8c81e02..181c65bd7c4f8 100644 --- a/lib/web_ui/flutter_js/src/skwasm_loader.js +++ b/lib/web_ui/flutter_js/src/skwasm_loader.js @@ -3,45 +3,35 @@ // found in the LICENSE file. import { createWasmInstantiator } from "./instantiate_wasm.js"; -import { joinPathSegments } from "./utils.js"; +import { resolveUrlWithSegments } from "./utils.js"; -export const loadSkwasm = (deps, config, browserEnvironment, baseUrl) => { - return new Promise((resolve, reject) => { - let skwasmUrl = joinPathSegments(baseUrl, "skwasm.js"); - if (deps.flutterTT.policy) { - skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl); - } - const wasmInstantiator = createWasmInstantiator(joinPathSegments(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); +export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => { + const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, "skwasm.js") + let skwasmUrl = rawSkwasmUrl; + if (deps.flutterTT.policy) { + skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl); + } + const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, "skwasm.wasm")); + const skwasm = await import(skwasmUrl); + return await skwasm.default({ + 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' })); } - }); - script.addEventListener("error", reject); - document.head.appendChild(script); + return url; + }, + // Because of the above workaround, the worker is just a blob and + // can't locate the main script using a relative path to itself, + // so we pass the main script location in. + mainScriptUrlOrBlob: rawSkwasmUrl, }); } diff --git a/lib/web_ui/flutter_js/src/utils.js b/lib/web_ui/flutter_js/src/utils.js index 690783b6d9431..f216b0246ab93 100644 --- a/lib/web_ui/flutter_js/src/utils.js +++ b/lib/web_ui/flutter_js/src/utils.js @@ -2,14 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export const baseUri = getBaseURI(); - -function getBaseURI() { - const base = document.querySelector("base"); - return (base && base.getAttribute("href")) || ""; +export function resolveUrlWithSegments(...segments) { + return new URL(joinPathSegments(...segments), document.baseURI).toString() } -export function joinPathSegments(...segments) { +function joinPathSegments(...segments) { return segments.filter((segment) => !!segment).map((segment, i) => { if (i === 0) { return stripRightSlashes(segment); @@ -54,5 +51,5 @@ export function getCanvaskitBaseUrl(config, buildConfig) { if (buildConfig.engineRevision && !buildConfig.useLocalCanvasKit) { return joinPathSegments("https://www.gstatic.com/flutter-canvaskit", buildConfig.engineRevision); } - return "/canvaskit"; + return "canvaskit"; } 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 c41da41b36aac..7a847220972c0 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -259,12 +259,13 @@ extension CanvasKitExtension on CanvasKit { ); } -@JS('window.CanvasKitInit') -external JSAny _CanvasKitInit(CanvasKitInitOptions options); +@JS() +@staticInterop +class CanvasKitModule {} -Future CanvasKitInit(CanvasKitInitOptions options) { - return js_util.promiseToFuture( - _CanvasKitInit(options).toObjectShallow); +extension CanvasKitModuleExtension on CanvasKitModule { + @JS('default') + external JSPromise defaultExport(CanvasKitInitOptions options); } typedef LocateFileCallback = String Function(String file, String unusedBase); @@ -3661,11 +3662,11 @@ String canvasKitWasmModuleUrl(String file, String canvasKitBase) => /// Downloads the CanvasKit JavaScript, then calls `CanvasKitInit` to download /// and intialize the CanvasKit wasm. Future downloadCanvasKit() async { - await _downloadOneOf(_canvasKitJsUrls); + final CanvasKitModule canvasKitModule = await _downloadOneOf(_canvasKitJsUrls); - final CanvasKit canvasKit = await CanvasKitInit(CanvasKitInitOptions( + final CanvasKit canvasKit = (await canvasKitModule.defaultExport(CanvasKitInitOptions( locateFile: createLocateFileCallback(canvasKitWasmModuleUrl), - )); + )).toDart) as CanvasKit; if (canvasKit.ParagraphBuilder.RequiresClientICU() && !browserSupportsCanvaskitChromium) { throw Exception( @@ -3681,10 +3682,12 @@ Future downloadCanvasKit() async { /// downloads it. /// /// If none of the URLs can be downloaded, throws an [Exception]. -Future _downloadOneOf(Iterable urls) async { +Future _downloadOneOf(Iterable urls) async { for (final String url in urls) { - if (await _downloadCanvasKitJs(url)) { - return; + try { + return await _downloadCanvasKitJs(url); + } catch (_) { + continue; } } @@ -3694,36 +3697,15 @@ Future _downloadOneOf(Iterable urls) async { ); } +String _resolveUrl(String url) { + return createDomURL(url, domWindow.document.baseUri).toJSString().toDart; +} + /// Downloads the CanvasKit JavaScript file at [url]. /// /// Returns a [Future] that completes with `true` if the CanvasKit JavaScript /// file was successfully downloaded, or `false` if it failed. -Future _downloadCanvasKitJs(String url) { - final DomHTMLScriptElement canvasKitScript = - createDomHTMLScriptElement(configuration.nonce); - canvasKitScript.src = createTrustedScriptUrl(url); - - final Completer canvasKitLoadCompleter = Completer(); - - late final DomEventListener loadCallback; - late final DomEventListener errorCallback; - - void loadEventHandler(DomEvent _) { - canvasKitScript.remove(); - canvasKitLoadCompleter.complete(true); - } - void errorEventHandler(DomEvent errorEvent) { - canvasKitScript.remove(); - canvasKitLoadCompleter.complete(false); - } - - loadCallback = createDomEventListener(loadEventHandler); - errorCallback = createDomEventListener(errorEventHandler); - - canvasKitScript.addEventListener('load', loadCallback); - canvasKitScript.addEventListener('error', errorCallback); - - domDocument.head!.appendChild(canvasKitScript); - - return canvasKitLoadCompleter.future; +Future _downloadCanvasKitJs(String url) async { + final JSAny scriptUrl = createTrustedScriptUrl(_resolveUrl(url)); + return (await importModule(scriptUrl).toDart) as CanvasKitModule; } diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index b81bba41245f2..3239c62173a94 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -2368,9 +2368,15 @@ extension DomPopStateEventExtension on DomPopStateEvent { dynamic get state => _state?.toObjectDeep; } -@JS() +@JS('URL') @staticInterop -class DomURL {} +class DomURL { + external factory DomURL.arg1(JSString url); + external factory DomURL.arg2(JSString url, JSString? base); +} + +DomURL createDomURL(String url, [String? base]) => + base == null ? DomURL.arg1(url.toJS) : DomURL.arg2(url.toJS, base.toJS); extension DomURLExtension on DomURL { @JS('createObjectURL') @@ -2381,6 +2387,9 @@ extension DomURLExtension on DomURL { @JS('revokeObjectURL') external JSVoid _revokeObjectURL(JSString url); void revokeObjectURL(String url) => _revokeObjectURL(url.toJS); + + @JS('toString') + external JSString toJSString(); } @JS('Blob') @@ -3383,16 +3392,16 @@ final DomTrustedTypePolicy _ttPolicy = domWindow.trustedTypes!.createPolicy( /// Converts a String `url` into a [DomTrustedScriptURL] object when the /// Trusted Types API is available, else returns the unmodified `url`. -Object createTrustedScriptUrl(String url) { +JSAny createTrustedScriptUrl(String url) { if (domWindow.trustedTypes != null) { // Pass `url` through Flutter Engine's TrustedType policy. final DomTrustedScriptURL trustedUrl = _ttPolicy.createScriptURL(url); assert(trustedUrl.url != '', 'URL: $url rejected by TrustedTypePolicy'); - return trustedUrl; + return trustedUrl as JSAny; } - return url; + return url.toJS; } DomMessageChannel createDomMessageChannel() => DomMessageChannel(); diff --git a/lib/web_ui/test/canvaskit/initialization/does_not_mock_module_exports_test.dart b/lib/web_ui/test/canvaskit/initialization/does_not_mock_module_exports_test.dart index b6676f2af4fcf..96e0c8a6d14d5 100644 --- a/lib/web_ui/test/canvaskit/initialization/does_not_mock_module_exports_test.dart +++ b/lib/web_ui/test/canvaskit/initialization/does_not_mock_module_exports_test.dart @@ -18,13 +18,6 @@ void testMain() { // Initialize CanvasKit... await bootstrapAndRunApp(); - // CanvasKitInit should be defined... - expect( - js_util.hasProperty(domWindow, 'CanvasKitInit'), - isTrue, - reason: 'CanvasKitInit should be defined on Window', - ); - // window.exports and window.module should be undefined! expect( js_util.hasProperty(domWindow, 'exports'), 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 d8960ad204237..bb91d9afe2bc6 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 @@ -56,15 +56,7 @@ Future bootstrapAndExtractConfig() { initializeEngine: ([JsFlutterConfiguration? config]) async => configCompleter.complete(config), runApp: () async {} ); - final FlutterLoader? loader = flutter?.loader; - if (loader == null || loader.isAutoStart) { - // TODO(jacksongardner): Unit tests under dart2wasm still use the old way which - // doesn't invoke flutter.js directly, so we autostart here. Once dart2wasm tests - // work with flutter.js, we can remove this code path. - bootstrap.autoStart(); - } else { - loader.didCreateEngineInitializer(bootstrap.prepareEngineInitializer()); - } + flutter!.loader!.didCreateEngineInitializer(bootstrap.prepareEngineInitializer()); return configCompleter.future; } diff --git a/lib/web_ui/test/engine/configuration_test.dart b/lib/web_ui/test/engine/configuration_test.dart index 24f748f42f3ef..7e4ba6caea4f4 100644 --- a/lib/web_ui/test/engine/configuration_test.dart +++ b/lib/web_ui/test/engine/configuration_test.dart @@ -28,10 +28,10 @@ void testMain() { test('legacy constructor initializes with a Js Object', () async { final FlutterConfiguration config = FlutterConfiguration.legacy( js_util.jsify({ - 'canvasKitBaseUrl': 'some_other_url/', + 'canvasKitBaseUrl': '/some_other_url/', }) as JsFlutterConfiguration); - expect(config.canvasKitBaseUrl, 'some_other_url/'); + expect(config.canvasKitBaseUrl, '/some_other_url/'); }); }); @@ -39,13 +39,13 @@ void testMain() { test('throws assertion error if already initialized from JS', () async { final FlutterConfiguration config = FlutterConfiguration.legacy( js_util.jsify({ - 'canvasKitBaseUrl': 'some_other_url/', + 'canvasKitBaseUrl': '/some_other_url/', }) as JsFlutterConfiguration); expect(() { config.setUserConfiguration( js_util.jsify({ - 'canvasKitBaseUrl': 'yet_another_url/', + 'canvasKitBaseUrl': '/yet_another_url/', }) as JsFlutterConfiguration); }, throwsAssertionError); }); @@ -55,10 +55,10 @@ void testMain() { config.setUserConfiguration( js_util.jsify({ - 'canvasKitBaseUrl': 'one_more_url/', + 'canvasKitBaseUrl': '/one_more_url/', }) as JsFlutterConfiguration); - expect(config.canvasKitBaseUrl, 'one_more_url/'); + expect(config.canvasKitBaseUrl, '/one_more_url/'); }); test('can receive non-existing properties without crashing', () async {