blob: db8b33159dc48dc1d04da98fafcec8528d0cf6ee [file] [log] [blame]
// 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;
}
}