Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 46c417b

Browse files
[CP-stable]Reland (x2) "Output .js files as ES6 modules. (#52023)" (#54446)
This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? - flutter/flutter#152953 - flutter/flutter#152844 ### Changelog Description: Fixes the loading of CanvasKit in a way that avoids app crashes and timeout issues. ### Impact Description: When CanvasKit is downloaded from the network (not cached), there's a high likelihood that the app crashes and displays a blank screen. ### Workaround: If the browser cache is enabled, users can refresh the page to get CanvasKit from cache and avoid the issue. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: - Run a sample flutter app using `--web-renderer=canvaskit`. - In Chrome's Network tab, check the `Disable cache` checkbox. - Reload the app a few times and make sure it's working and there are no errors in the console.
1 parent f9db4e5 commit 46c417b

File tree

12 files changed

+97
-146
lines changed

12 files changed

+97
-146
lines changed

DEPS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ allowed_hosts = [
277277
]
278278

279279
deps = {
280-
'src': 'https://github.com/flutter/buildroot.git' + '@' + '8c2d66fa4e6298894425f5bdd0591bc5b1154c53',
280+
'src': 'https://github.com/flutter/buildroot.git' + '@' + 'e265c359126b24351f534080fb22edaa159f2215',
281281

282282
'src/flutter/third_party/depot_tools':
283283
Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '580b4ff3f5cd0dcaa2eacda28cefe0f45320e8f7',

lib/web_ui/dev/test_platform.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ class BrowserPlatform extends PlatformPlugin {
575575
// Some of our tests rely on color emoji
576576
useColorEmoji: true,
577577
canvasKitVariant: "${getCanvasKitVariant()}",
578+
canvasKitBaseUrl: "/canvaskit",
578579
},
579580
});
580581
</script>

lib/web_ui/flutter_js/src/canvaskit_loader.js

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,33 @@
33
// found in the LICENSE file.
44

55
import { createWasmInstantiator } from "./instantiate_wasm.js";
6-
import { joinPathSegments } from "./utils.js";
6+
import { resolveUrlWithSegments } from "./utils.js";
77

88
export const loadCanvasKit = (deps, config, browserEnvironment, canvasKitBaseUrl) => {
9-
if (window.flutterCanvasKit) {
10-
// The user has set this global variable ahead of time, so we just return that.
11-
return Promise.resolve(window.flutterCanvasKit);
12-
}
13-
window.flutterCanvasKitLoaded = new Promise((resolve, reject) => {
9+
window.flutterCanvasKitLoaded = (async () => {
10+
if (window.flutterCanvasKit) {
11+
// The user has set this global variable ahead of time, so we just return that.
12+
return window.flutterCanvasKit;
13+
}
1414
const supportsChromiumCanvasKit = browserEnvironment.hasChromiumBreakIterators && browserEnvironment.hasImageCodecs;
1515
if (!supportsChromiumCanvasKit && config.canvasKitVariant == "chromium") {
1616
throw "Chromium CanvasKit variant specifically requested, but unsupported in this browser";
1717
}
1818
const useChromiumCanvasKit = supportsChromiumCanvasKit && (config.canvasKitVariant !== "full");
1919
let baseUrl = canvasKitBaseUrl;
2020
if (useChromiumCanvasKit) {
21-
baseUrl = joinPathSegments(baseUrl, "chromium");
21+
baseUrl = resolveUrlWithSegments(baseUrl, "chromium");
2222
}
23-
let canvasKitUrl = joinPathSegments(baseUrl, "canvaskit.js");
23+
let canvasKitUrl = resolveUrlWithSegments(baseUrl, "canvaskit.js");
2424
if (deps.flutterTT.policy) {
2525
canvasKitUrl = deps.flutterTT.policy.createScriptURL(canvasKitUrl);
2626
}
27-
const wasmInstantiator = createWasmInstantiator(joinPathSegments(baseUrl, "canvaskit.wasm"));
28-
const script = document.createElement("script");
29-
script.src = canvasKitUrl;
30-
if (config.nonce) {
31-
script.nonce = config.nonce;
32-
}
33-
script.addEventListener("load", async () => {
34-
try {
35-
const canvasKit = await CanvasKitInit({
36-
instantiateWasm: wasmInstantiator,
37-
});
38-
window.flutterCanvasKit = canvasKit;
39-
resolve(canvasKit);
40-
} catch (e) {
41-
reject(e);
42-
}
27+
const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, "canvaskit.wasm"));
28+
const canvasKitModule = await import(canvasKitUrl);
29+
window.flutterCanvasKit = await canvasKitModule.default({
30+
instantiateWasm: wasmInstantiator,
4331
});
44-
script.addEventListener("error", reject);
45-
document.head.appendChild(script);
46-
});
32+
return window.flutterCanvasKit;
33+
})();
4734
return window.flutterCanvasKitLoaded;
4835
}

lib/web_ui/flutter_js/src/entrypoint_loader.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import { baseUri, joinPathSegments } from "./utils.js";
5+
import { resolveUrlWithSegments } from "./utils.js";
66

77
/**
88
* Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
@@ -37,7 +37,7 @@ export class FlutterEntrypointLoader {
3737
* Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
3838
*/
3939
async loadEntrypoint(options) {
40-
const { entrypointUrl = joinPathSegments(baseUri, "main.dart.js"), onEntrypointLoaded, nonce } =
40+
const { entrypointUrl = resolveUrlWithSegments("main.dart.js"), onEntrypointLoaded, nonce } =
4141
options || {};
4242
return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
4343
}
@@ -68,7 +68,7 @@ export class FlutterEntrypointLoader {
6868
return this._loadWasmEntrypoint(build, deps, entryPointBaseUrl, onEntrypointLoaded);
6969
} else {
7070
const mainPath = build.mainJsPath ?? "main.dart.js";
71-
const entrypointUrl = joinPathSegments(baseUri, entryPointBaseUrl, mainPath);
71+
const entrypointUrl = resolveUrlWithSegments(entryPointBaseUrl, mainPath);
7272
return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
7373
}
7474
}
@@ -148,8 +148,8 @@ export class FlutterEntrypointLoader {
148148

149149
this._onEntrypointLoaded = onEntrypointLoaded;
150150
const { mainWasmPath, jsSupportRuntimePath } = build;
151-
const moduleUri = joinPathSegments(baseUri, entrypointBaseUrl, mainWasmPath);
152-
let jsSupportRuntimeUri = joinPathSegments(baseUri, entrypointBaseUrl, jsSupportRuntimePath);
151+
const moduleUri = resolveUrlWithSegments(entrypointBaseUrl, mainWasmPath);
152+
let jsSupportRuntimeUri = resolveUrlWithSegments(entrypointBaseUrl, jsSupportRuntimePath);
153153
if (this._ttPolicy != null) {
154154
jsSupportRuntimeUri = this._ttPolicy.createScriptURL(jsSupportRuntimeUri);
155155
}

lib/web_ui/flutter_js/src/service_worker_loader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import { baseUri, joinPathSegments } from "./utils.js";
5+
import { resolveUrlWithSegments } from "./utils.js";
66

77
/**
88
* Wraps `promise` in a timeout of the given `duration` in ms.
@@ -78,7 +78,7 @@ export class FlutterServiceWorkerLoader {
7878
}
7979
const {
8080
serviceWorkerVersion,
81-
serviceWorkerUrl = joinPathSegments(baseUri, `flutter_service_worker.js?v=${serviceWorkerVersion}`),
81+
serviceWorkerUrl = resolveUrlWithSegments(`flutter_service_worker.js?v=${serviceWorkerVersion}`),
8282
timeoutMillis = 4000,
8383
} = settings;
8484
// Apply the TrustedTypes policy, if present.

lib/web_ui/flutter_js/src/skwasm_loader.js

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,35 @@
33
// found in the LICENSE file.
44

55
import { createWasmInstantiator } from "./instantiate_wasm.js";
6-
import { joinPathSegments } from "./utils.js";
6+
import { resolveUrlWithSegments } from "./utils.js";
77

8-
export const loadSkwasm = (deps, config, browserEnvironment, baseUrl) => {
9-
return new Promise((resolve, reject) => {
10-
let skwasmUrl = joinPathSegments(baseUrl, "skwasm.js");
11-
if (deps.flutterTT.policy) {
12-
skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
13-
}
14-
const wasmInstantiator = createWasmInstantiator(joinPathSegments(baseUrl, "skwasm.wasm"));
15-
const script = document.createElement("script");
16-
script.src = skwasmUrl;
17-
if (config.nonce) {
18-
script.nonce = config.nonce;
19-
}
20-
script.addEventListener("load", async () => {
21-
try {
22-
const skwasmInstance = await skwasm({
23-
instantiateWasm: wasmInstantiator,
24-
locateFile: (fileName, scriptDirectory) => {
25-
// When hosted via a CDN or some other url that is not the same
26-
// origin as the main script of the page, we will fail to create
27-
// a web worker with the .worker.js script. This workaround will
28-
// make sure that the worker JS can be loaded regardless of where
29-
// it is hosted.
30-
const url = scriptDirectory + fileName;
31-
if (url.endsWith(".worker.js")) {
32-
return URL.createObjectURL(new Blob(
33-
[`importScripts("${url}");`],
34-
{ "type": "application/javascript" }));
35-
}
36-
return url;
37-
}
38-
});
39-
resolve(skwasmInstance);
40-
} catch (e) {
41-
reject(e);
8+
export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
9+
const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, "skwasm.js")
10+
let skwasmUrl = rawSkwasmUrl;
11+
if (deps.flutterTT.policy) {
12+
skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
13+
}
14+
const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, "skwasm.wasm"));
15+
const skwasm = await import(skwasmUrl);
16+
return await skwasm.default({
17+
instantiateWasm: wasmInstantiator,
18+
locateFile: (fileName, scriptDirectory) => {
19+
// When hosted via a CDN or some other url that is not the same
20+
// origin as the main script of the page, we will fail to create
21+
// a web worker with the .worker.js script. This workaround will
22+
// make sure that the worker JS can be loaded regardless of where
23+
// it is hosted.
24+
const url = scriptDirectory + fileName;
25+
if (url.endsWith('.worker.js')) {
26+
return URL.createObjectURL(new Blob(
27+
[`importScripts('${url}');`],
28+
{ 'type': 'application/javascript' }));
4229
}
43-
});
44-
script.addEventListener("error", reject);
45-
document.head.appendChild(script);
30+
return url;
31+
},
32+
// Because of the above workaround, the worker is just a blob and
33+
// can't locate the main script using a relative path to itself,
34+
// so we pass the main script location in.
35+
mainScriptUrlOrBlob: rawSkwasmUrl,
4636
});
4737
}

lib/web_ui/flutter_js/src/utils.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
export const baseUri = getBaseURI();
6-
7-
function getBaseURI() {
8-
const base = document.querySelector("base");
9-
return (base && base.getAttribute("href")) || "";
5+
export function resolveUrlWithSegments(...segments) {
6+
return new URL(joinPathSegments(...segments), document.baseURI).toString()
107
}
118

12-
export function joinPathSegments(...segments) {
9+
function joinPathSegments(...segments) {
1310
return segments.filter((segment) => !!segment).map((segment, i) => {
1411
if (i === 0) {
1512
return stripRightSlashes(segment);
@@ -54,5 +51,5 @@ export function getCanvaskitBaseUrl(config, buildConfig) {
5451
if (buildConfig.engineRevision && !buildConfig.useLocalCanvasKit) {
5552
return joinPathSegments("https://www.gstatic.com/flutter-canvaskit", buildConfig.engineRevision);
5653
}
57-
return "/canvaskit";
54+
return "canvaskit";
5855
}

lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,13 @@ extension CanvasKitExtension on CanvasKit {
259259
);
260260
}
261261

262-
@JS('window.CanvasKitInit')
263-
external JSAny _CanvasKitInit(CanvasKitInitOptions options);
262+
@JS()
263+
@staticInterop
264+
class CanvasKitModule {}
264265

265-
Future<CanvasKit> CanvasKitInit(CanvasKitInitOptions options) {
266-
return js_util.promiseToFuture<CanvasKit>(
267-
_CanvasKitInit(options).toObjectShallow);
266+
extension CanvasKitModuleExtension on CanvasKitModule {
267+
@JS('default')
268+
external JSPromise<JSAny> defaultExport(CanvasKitInitOptions options);
268269
}
269270

270271
typedef LocateFileCallback = String Function(String file, String unusedBase);
@@ -3661,11 +3662,11 @@ String canvasKitWasmModuleUrl(String file, String canvasKitBase) =>
36613662
/// Downloads the CanvasKit JavaScript, then calls `CanvasKitInit` to download
36623663
/// and intialize the CanvasKit wasm.
36633664
Future<CanvasKit> downloadCanvasKit() async {
3664-
await _downloadOneOf(_canvasKitJsUrls);
3665+
final CanvasKitModule canvasKitModule = await _downloadOneOf(_canvasKitJsUrls);
36653666

3666-
final CanvasKit canvasKit = await CanvasKitInit(CanvasKitInitOptions(
3667+
final CanvasKit canvasKit = (await canvasKitModule.defaultExport(CanvasKitInitOptions(
36673668
locateFile: createLocateFileCallback(canvasKitWasmModuleUrl),
3668-
));
3669+
)).toDart) as CanvasKit;
36693670

36703671
if (canvasKit.ParagraphBuilder.RequiresClientICU() && !browserSupportsCanvaskitChromium) {
36713672
throw Exception(
@@ -3681,10 +3682,12 @@ Future<CanvasKit> downloadCanvasKit() async {
36813682
/// downloads it.
36823683
///
36833684
/// If none of the URLs can be downloaded, throws an [Exception].
3684-
Future<void> _downloadOneOf(Iterable<String> urls) async {
3685+
Future<CanvasKitModule> _downloadOneOf(Iterable<String> urls) async {
36853686
for (final String url in urls) {
3686-
if (await _downloadCanvasKitJs(url)) {
3687-
return;
3687+
try {
3688+
return await _downloadCanvasKitJs(url);
3689+
} catch (_) {
3690+
continue;
36883691
}
36893692
}
36903693

@@ -3694,36 +3697,15 @@ Future<void> _downloadOneOf(Iterable<String> urls) async {
36943697
);
36953698
}
36963699

3700+
String _resolveUrl(String url) {
3701+
return createDomURL(url, domWindow.document.baseUri).toJSString().toDart;
3702+
}
3703+
36973704
/// Downloads the CanvasKit JavaScript file at [url].
36983705
///
36993706
/// Returns a [Future] that completes with `true` if the CanvasKit JavaScript
37003707
/// file was successfully downloaded, or `false` if it failed.
3701-
Future<bool> _downloadCanvasKitJs(String url) {
3702-
final DomHTMLScriptElement canvasKitScript =
3703-
createDomHTMLScriptElement(configuration.nonce);
3704-
canvasKitScript.src = createTrustedScriptUrl(url);
3705-
3706-
final Completer<bool> canvasKitLoadCompleter = Completer<bool>();
3707-
3708-
late final DomEventListener loadCallback;
3709-
late final DomEventListener errorCallback;
3710-
3711-
void loadEventHandler(DomEvent _) {
3712-
canvasKitScript.remove();
3713-
canvasKitLoadCompleter.complete(true);
3714-
}
3715-
void errorEventHandler(DomEvent errorEvent) {
3716-
canvasKitScript.remove();
3717-
canvasKitLoadCompleter.complete(false);
3718-
}
3719-
3720-
loadCallback = createDomEventListener(loadEventHandler);
3721-
errorCallback = createDomEventListener(errorEventHandler);
3722-
3723-
canvasKitScript.addEventListener('load', loadCallback);
3724-
canvasKitScript.addEventListener('error', errorCallback);
3725-
3726-
domDocument.head!.appendChild(canvasKitScript);
3727-
3728-
return canvasKitLoadCompleter.future;
3708+
Future<CanvasKitModule> _downloadCanvasKitJs(String url) async {
3709+
final JSAny scriptUrl = createTrustedScriptUrl(_resolveUrl(url));
3710+
return (await importModule(scriptUrl).toDart) as CanvasKitModule;
37293711
}

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,9 +2368,15 @@ extension DomPopStateEventExtension on DomPopStateEvent {
23682368
dynamic get state => _state?.toObjectDeep;
23692369
}
23702370

2371-
@JS()
2371+
@JS('URL')
23722372
@staticInterop
2373-
class DomURL {}
2373+
class DomURL {
2374+
external factory DomURL.arg1(JSString url);
2375+
external factory DomURL.arg2(JSString url, JSString? base);
2376+
}
2377+
2378+
DomURL createDomURL(String url, [String? base]) =>
2379+
base == null ? DomURL.arg1(url.toJS) : DomURL.arg2(url.toJS, base.toJS);
23742380

23752381
extension DomURLExtension on DomURL {
23762382
@JS('createObjectURL')
@@ -2381,6 +2387,9 @@ extension DomURLExtension on DomURL {
23812387
@JS('revokeObjectURL')
23822388
external JSVoid _revokeObjectURL(JSString url);
23832389
void revokeObjectURL(String url) => _revokeObjectURL(url.toJS);
2390+
2391+
@JS('toString')
2392+
external JSString toJSString();
23842393
}
23852394

23862395
@JS('Blob')
@@ -3383,16 +3392,16 @@ final DomTrustedTypePolicy _ttPolicy = domWindow.trustedTypes!.createPolicy(
33833392

33843393
/// Converts a String `url` into a [DomTrustedScriptURL] object when the
33853394
/// Trusted Types API is available, else returns the unmodified `url`.
3386-
Object createTrustedScriptUrl(String url) {
3395+
JSAny createTrustedScriptUrl(String url) {
33873396
if (domWindow.trustedTypes != null) {
33883397
// Pass `url` through Flutter Engine's TrustedType policy.
33893398
final DomTrustedScriptURL trustedUrl = _ttPolicy.createScriptURL(url);
33903399

33913400
assert(trustedUrl.url != '', 'URL: $url rejected by TrustedTypePolicy');
33923401

3393-
return trustedUrl;
3402+
return trustedUrl as JSAny;
33943403
}
3395-
return url;
3404+
return url.toJS;
33963405
}
33973406

33983407
DomMessageChannel createDomMessageChannel() => DomMessageChannel();

lib/web_ui/test/canvaskit/initialization/does_not_mock_module_exports_test.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@ void testMain() {
1818
// Initialize CanvasKit...
1919
await bootstrapAndRunApp();
2020

21-
// CanvasKitInit should be defined...
22-
expect(
23-
js_util.hasProperty(domWindow, 'CanvasKitInit'),
24-
isTrue,
25-
reason: 'CanvasKitInit should be defined on Window',
26-
);
27-
2821
// window.exports and window.module should be undefined!
2922
expect(
3023
js_util.hasProperty(domWindow, 'exports'),

0 commit comments

Comments
 (0)