mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[web] Move JS content to its own .js
files (#117691)
This commit is contained in:
parent
3f5b105fc7
commit
e03029ef6a
@ -9,6 +9,7 @@ import 'base/common.dart';
|
|||||||
import 'base/file_system.dart';
|
import 'base/file_system.dart';
|
||||||
import 'base/os.dart';
|
import 'base/os.dart';
|
||||||
import 'base/platform.dart';
|
import 'base/platform.dart';
|
||||||
|
import 'base/user_messages.dart';
|
||||||
import 'base/utils.dart';
|
import 'base/utils.dart';
|
||||||
import 'build_info.dart';
|
import 'build_info.dart';
|
||||||
import 'cache.dart';
|
import 'cache.dart';
|
||||||
@ -62,6 +63,9 @@ enum Artifact {
|
|||||||
/// Tools related to subsetting or icon font files.
|
/// Tools related to subsetting or icon font files.
|
||||||
fontSubset,
|
fontSubset,
|
||||||
constFinder,
|
constFinder,
|
||||||
|
|
||||||
|
/// The location of file generators.
|
||||||
|
flutterToolsFileGenerators,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A subset of [Artifact]s that are platform and build mode independent
|
/// A subset of [Artifact]s that are platform and build mode independent
|
||||||
@ -202,6 +206,8 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod
|
|||||||
return 'font-subset$exe';
|
return 'font-subset$exe';
|
||||||
case Artifact.constFinder:
|
case Artifact.constFinder:
|
||||||
return 'const_finder.dart.snapshot';
|
return 'const_finder.dart.snapshot';
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,6 +531,8 @@ class CachedArtifacts implements Artifacts {
|
|||||||
case Artifact.windowsCppClientWrapper:
|
case Artifact.windowsCppClientWrapper:
|
||||||
case Artifact.windowsDesktopPath:
|
case Artifact.windowsDesktopPath:
|
||||||
return _getHostArtifactPath(artifact, platform, mode);
|
return _getHostArtifactPath(artifact, platform, mode);
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,6 +570,8 @@ class CachedArtifacts implements Artifacts {
|
|||||||
case Artifact.windowsCppClientWrapper:
|
case Artifact.windowsCppClientWrapper:
|
||||||
case Artifact.windowsDesktopPath:
|
case Artifact.windowsDesktopPath:
|
||||||
return _getHostArtifactPath(artifact, platform, mode);
|
return _getHostArtifactPath(artifact, platform, mode);
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,6 +621,8 @@ class CachedArtifacts implements Artifacts {
|
|||||||
case Artifact.windowsCppClientWrapper:
|
case Artifact.windowsCppClientWrapper:
|
||||||
case Artifact.windowsDesktopPath:
|
case Artifact.windowsDesktopPath:
|
||||||
return _getHostArtifactPath(artifact, platform, mode);
|
return _getHostArtifactPath(artifact, platform, mode);
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,6 +697,8 @@ class CachedArtifacts implements Artifacts {
|
|||||||
case Artifact.fuchsiaFlutterRunner:
|
case Artifact.fuchsiaFlutterRunner:
|
||||||
case Artifact.fuchsiaKernelCompiler:
|
case Artifact.fuchsiaKernelCompiler:
|
||||||
throw StateError('Artifact $artifact not available for platform $platform.');
|
throw StateError('Artifact $artifact not available for platform $platform.');
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -952,6 +966,8 @@ class CachedLocalEngineArtifacts implements Artifacts {
|
|||||||
case Artifact.dart2wasmSnapshot:
|
case Artifact.dart2wasmSnapshot:
|
||||||
case Artifact.frontendServerSnapshotForEngineDartSdk:
|
case Artifact.frontendServerSnapshotForEngineDartSdk:
|
||||||
return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'snapshots', artifactFileName);
|
return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'snapshots', artifactFileName);
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1099,6 +1115,7 @@ class CachedLocalWebSdkArtifacts implements Artifacts {
|
|||||||
case Artifact.fuchsiaFlutterRunner:
|
case Artifact.fuchsiaFlutterRunner:
|
||||||
case Artifact.fontSubset:
|
case Artifact.fontSubset:
|
||||||
case Artifact.constFinder:
|
case Artifact.constFinder:
|
||||||
|
case Artifact.flutterToolsFileGenerators:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1298,6 +1315,11 @@ class _TestArtifacts implements Artifacts {
|
|||||||
BuildMode? mode,
|
BuildMode? mode,
|
||||||
EnvironmentType? environmentType,
|
EnvironmentType? environmentType,
|
||||||
}) {
|
}) {
|
||||||
|
// The path to file generators is the same even in the test environment.
|
||||||
|
if (artifact == Artifact.flutterToolsFileGenerators) {
|
||||||
|
return _getFileGeneratorsPath();
|
||||||
|
}
|
||||||
|
|
||||||
final StringBuffer buffer = StringBuffer();
|
final StringBuffer buffer = StringBuffer();
|
||||||
buffer.write(artifact);
|
buffer.write(artifact);
|
||||||
if (platform != null) {
|
if (platform != null) {
|
||||||
@ -1340,3 +1362,20 @@ class _TestLocalEngine extends _TestArtifacts {
|
|||||||
@override
|
@override
|
||||||
final LocalEngineInfo localEngineInfo;
|
final LocalEngineInfo localEngineInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getFileGeneratorsPath() {
|
||||||
|
final String flutterRoot = Cache.defaultFlutterRoot(
|
||||||
|
fileSystem: globals.localFileSystem,
|
||||||
|
platform: const LocalPlatform(),
|
||||||
|
userMessages: UserMessages(),
|
||||||
|
);
|
||||||
|
return globals.localFileSystem.path.join(
|
||||||
|
flutterRoot,
|
||||||
|
'packages',
|
||||||
|
'flutter_tools',
|
||||||
|
'lib',
|
||||||
|
'src',
|
||||||
|
'web',
|
||||||
|
'file_generators',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -529,7 +529,10 @@ class WebBuiltInAssets extends Target {
|
|||||||
|
|
||||||
// Write the flutter.js file
|
// Write the flutter.js file
|
||||||
final File flutterJsFile = environment.outputDir.childFile('flutter.js');
|
final File flutterJsFile = environment.outputDir.childFile('flutter.js');
|
||||||
flutterJsFile.writeAsStringSync(flutter_js.generateFlutterJsFile());
|
final String fileGeneratorsPath =
|
||||||
|
globals.artifacts!.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
flutterJsFile.writeAsStringSync(
|
||||||
|
flutter_js.generateFlutterJsFile(fileGeneratorsPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,7 +601,10 @@ class WebServiceWorker extends Target {
|
|||||||
final ServiceWorkerStrategy serviceWorkerStrategy = _serviceWorkerStrategyFromString(
|
final ServiceWorkerStrategy serviceWorkerStrategy = _serviceWorkerStrategyFromString(
|
||||||
environment.defines[kServiceWorkerStrategy],
|
environment.defines[kServiceWorkerStrategy],
|
||||||
);
|
);
|
||||||
|
final String fileGeneratorsPath =
|
||||||
|
globals.artifacts!.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
final String serviceWorker = generateServiceWorker(
|
final String serviceWorker = generateServiceWorker(
|
||||||
|
fileGeneratorsPath,
|
||||||
urlToHash,
|
urlToHash,
|
||||||
<String>[
|
<String>[
|
||||||
'main.dart.js',
|
'main.dart.js',
|
||||||
|
@ -828,7 +828,10 @@ class WebDevFS implements DevFS {
|
|||||||
'stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
|
'stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
|
||||||
webAssetServer.writeFile(
|
webAssetServer.writeFile(
|
||||||
'manifest.json', '{"info":"manifest not generated in run mode."}');
|
'manifest.json', '{"info":"manifest not generated in run mode."}');
|
||||||
webAssetServer.writeFile('flutter.js', flutter_js.generateFlutterJsFile());
|
final String fileGeneratorsPath = globals.artifacts!
|
||||||
|
.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
webAssetServer.writeFile(
|
||||||
|
'flutter.js', flutter_js.generateFlutterJsFile(fileGeneratorsPath));
|
||||||
webAssetServer.writeFile('flutter_service_worker.js',
|
webAssetServer.writeFile('flutter_service_worker.js',
|
||||||
'// Service worker not loaded in run mode.');
|
'// Service worker not loaded in run mode.');
|
||||||
webAssetServer.writeFile(
|
webAssetServer.writeFile(
|
||||||
|
@ -2,388 +2,17 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import '../../globals.dart' as globals;
|
||||||
|
|
||||||
/// Generates the flutter.js file.
|
/// Generates the flutter.js file.
|
||||||
///
|
///
|
||||||
/// flutter.js should be completely static, so **do not use any parameter or
|
/// flutter.js should be completely static, so **do not use any parameter or
|
||||||
/// environment variable to generate this file**.
|
/// environment variable to generate this file**.
|
||||||
String generateFlutterJsFile() {
|
String generateFlutterJsFile(String fileGeneratorsPath) {
|
||||||
return r'''
|
final String flutterJsPath = globals.localFileSystem.path.join(
|
||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
fileGeneratorsPath,
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
'js',
|
||||||
// found in the LICENSE file.
|
'flutter.js',
|
||||||
|
);
|
||||||
if (!_flutter) {
|
return globals.localFileSystem.file(flutterJsPath).readAsStringSync();
|
||||||
var _flutter = {};
|
|
||||||
}
|
|
||||||
_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 || [
|
|
||||||
/\.dart\.js$/,
|
|
||||||
/^flutter_service_worker.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 (!("serviceWorker" in navigator) || settings == null) {
|
|
||||||
// In the future, settings = null -> uninstall service worker?
|
|
||||||
return Promise.reject(
|
|
||||||
new Error("Service worker not supported (or configured).")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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(this._getNewServiceWorker)
|
|
||||||
.then(this._waitForServiceWorkerActivation);
|
|
||||||
|
|
||||||
// Timeout race promise
|
|
||||||
return timeout(
|
|
||||||
serviceWorkerActivation,
|
|
||||||
timeoutMillis,
|
|
||||||
"prepareServiceWorker"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the latest service worker for the given `serviceWorkerRegistrationPromise`.
|
|
||||||
*
|
|
||||||
* This might return the current service worker, if there's no new service worker
|
|
||||||
* awaiting to be installed/updated.
|
|
||||||
*
|
|
||||||
* @param {Promise<ServiceWorkerRegistration>} serviceWorkerRegistrationPromise
|
|
||||||
* @returns {Promise<ServiceWorker>}
|
|
||||||
*/
|
|
||||||
async _getNewServiceWorker(serviceWorkerRegistrationPromise) {
|
|
||||||
const reg = await serviceWorkerRegistrationPromise;
|
|
||||||
|
|
||||||
if (!reg.active && (reg.installing || reg.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 reg.installing || reg.waiting;
|
|
||||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
|
||||||
// When the app updates the serviceWorkerVersion changes, so we
|
|
||||||
// need to ask the service worker to update.
|
|
||||||
return reg.update().then((newReg) => {
|
|
||||||
console.debug("Updating service worker.");
|
|
||||||
return newReg.installing || newReg.waiting || newReg.active;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.debug("Loading from existing service worker.");
|
|
||||||
return reg.active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Promise that resolves when the `latestServiceWorker` changes its
|
|
||||||
* state to "activated".
|
|
||||||
*
|
|
||||||
* @param {Promise<ServiceWorker>} latestServiceWorkerPromise
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async _waitForServiceWorkerActivation(latestServiceWorkerPromise) {
|
|
||||||
const serviceWorker = await latestServiceWorkerPromise;
|
|
||||||
|
|
||||||
if (!serviceWorker || serviceWorker.state == "activated") {
|
|
||||||
if (!serviceWorker) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error("Cannot activate a null service worker!")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.debug("Service worker already active.");
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 } =
|
|
||||||
options || {};
|
|
||||||
|
|
||||||
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
const useCallback = typeof onEntrypointLoaded === "function";
|
|
||||||
|
|
||||||
if (!this._scriptLoaded) {
|
|
||||||
this._scriptLoaded = true;
|
|
||||||
const scriptTag = this._createScriptTag(entrypointUrl);
|
|
||||||
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) {
|
|
||||||
const scriptTag = document.createElement("script");
|
|
||||||
scriptTag.type = "application/javascript";
|
|
||||||
// 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();
|
|
||||||
})();
|
|
||||||
|
|
||||||
''';
|
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import '../../globals.dart' as globals;
|
||||||
|
|
||||||
/// The caching strategy for the generated service worker.
|
/// The caching strategy for the generated service worker.
|
||||||
enum ServiceWorkerStrategy {
|
enum ServiceWorkerStrategy {
|
||||||
/// Download the app shell eagerly and all other assets lazily.
|
/// Download the app shell eagerly and all other assets lazily.
|
||||||
/// Prefer the offline cached version.
|
/// Prefer the offline cached version.
|
||||||
offlineFirst,
|
offlineFirst,
|
||||||
|
|
||||||
/// Do not generate a service worker,
|
/// Do not generate a service worker,
|
||||||
none,
|
none,
|
||||||
}
|
}
|
||||||
@ -18,6 +21,7 @@ enum ServiceWorkerStrategy {
|
|||||||
/// invalidation will automatically reactivate workers whenever a new
|
/// invalidation will automatically reactivate workers whenever a new
|
||||||
/// version is deployed.
|
/// version is deployed.
|
||||||
String generateServiceWorker(
|
String generateServiceWorker(
|
||||||
|
String fileGeneratorsPath,
|
||||||
Map<String, String> resources,
|
Map<String, String> resources,
|
||||||
List<String> coreBundle, {
|
List<String> coreBundle, {
|
||||||
required ServiceWorkerStrategy serviceWorkerStrategy,
|
required ServiceWorkerStrategy serviceWorkerStrategy,
|
||||||
@ -25,185 +29,21 @@ String generateServiceWorker(
|
|||||||
if (serviceWorkerStrategy == ServiceWorkerStrategy.none) {
|
if (serviceWorkerStrategy == ServiceWorkerStrategy.none) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return '''
|
|
||||||
'use strict';
|
|
||||||
const MANIFEST = 'flutter-app-manifest';
|
|
||||||
const TEMP = 'flutter-temp-cache';
|
|
||||||
const CACHE_NAME = 'flutter-app-cache';
|
|
||||||
const RESOURCES = {
|
|
||||||
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The application shell files that are downloaded before a service worker can
|
final String flutterServiceWorkerJsPath = globals.localFileSystem.path.join(
|
||||||
// start.
|
fileGeneratorsPath,
|
||||||
const CORE = [
|
'js',
|
||||||
${coreBundle.map((String file) => '"$file"').join(',\n')}];
|
'flutter_service_worker.js',
|
||||||
// During install, the TEMP cache is populated with the application shell files.
|
|
||||||
self.addEventListener("install", (event) => {
|
|
||||||
self.skipWaiting();
|
|
||||||
return event.waitUntil(
|
|
||||||
caches.open(TEMP).then((cache) => {
|
|
||||||
return cache.addAll(
|
|
||||||
CORE.map((value) => new Request(value, {'cache': 'reload'})));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
});
|
return globals.localFileSystem
|
||||||
|
.file(flutterServiceWorkerJsPath)
|
||||||
// During activate, the cache is populated with the temp files downloaded in
|
.readAsStringSync()
|
||||||
// install. If this service worker is upgrading from one with a saved
|
.replaceAll(
|
||||||
// MANIFEST, then use this to retain unchanged resource files.
|
r'$$RESOURCES_MAP',
|
||||||
self.addEventListener("activate", function(event) {
|
'{${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}}',
|
||||||
return event.waitUntil(async function() {
|
)
|
||||||
try {
|
.replaceAll(
|
||||||
var contentCache = await caches.open(CACHE_NAME);
|
r'$$CORE_LIST',
|
||||||
var tempCache = await caches.open(TEMP);
|
'[${coreBundle.map((String file) => '"$file"').join(',\n')}]',
|
||||||
var manifestCache = await caches.open(MANIFEST);
|
);
|
||||||
var manifest = await manifestCache.match('manifest');
|
|
||||||
// When there is no prior manifest, clear the entire cache.
|
|
||||||
if (!manifest) {
|
|
||||||
await caches.delete(CACHE_NAME);
|
|
||||||
contentCache = await caches.open(CACHE_NAME);
|
|
||||||
for (var request of await tempCache.keys()) {
|
|
||||||
var response = await tempCache.match(request);
|
|
||||||
await contentCache.put(request, response);
|
|
||||||
}
|
|
||||||
await caches.delete(TEMP);
|
|
||||||
// Save the manifest to make future upgrades efficient.
|
|
||||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
|
||||||
// Claim client to enable caching on first launch
|
|
||||||
self.clients.claim();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var oldManifest = await manifest.json();
|
|
||||||
var origin = self.location.origin;
|
|
||||||
for (var request of await contentCache.keys()) {
|
|
||||||
var key = request.url.substring(origin.length + 1);
|
|
||||||
if (key == "") {
|
|
||||||
key = "/";
|
|
||||||
}
|
|
||||||
// If a resource from the old manifest is not in the new cache, or if
|
|
||||||
// the MD5 sum has changed, delete it. Otherwise the resource is left
|
|
||||||
// in the cache and can be reused by the new service worker.
|
|
||||||
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
|
|
||||||
await contentCache.delete(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Populate the cache with the app shell TEMP files, potentially overwriting
|
|
||||||
// cache files preserved above.
|
|
||||||
for (var request of await tempCache.keys()) {
|
|
||||||
var response = await tempCache.match(request);
|
|
||||||
await contentCache.put(request, response);
|
|
||||||
}
|
|
||||||
await caches.delete(TEMP);
|
|
||||||
// Save the manifest to make future upgrades efficient.
|
|
||||||
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
|
||||||
// Claim client to enable caching on first launch
|
|
||||||
self.clients.claim();
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
// On an unhandled exception the state of the cache cannot be guaranteed.
|
|
||||||
console.error('Failed to upgrade service worker: ' + err);
|
|
||||||
await caches.delete(CACHE_NAME);
|
|
||||||
await caches.delete(TEMP);
|
|
||||||
await caches.delete(MANIFEST);
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
});
|
|
||||||
|
|
||||||
// The fetch handler redirects requests for RESOURCE files to the service
|
|
||||||
// worker cache.
|
|
||||||
self.addEventListener("fetch", (event) => {
|
|
||||||
if (event.request.method !== 'GET') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var origin = self.location.origin;
|
|
||||||
var key = event.request.url.substring(origin.length + 1);
|
|
||||||
// Redirect URLs to the index.html
|
|
||||||
if (key.indexOf('?v=') != -1) {
|
|
||||||
key = key.split('?v=')[0];
|
|
||||||
}
|
|
||||||
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
|
|
||||||
key = '/';
|
|
||||||
}
|
|
||||||
// If the URL is not the RESOURCE list then return to signal that the
|
|
||||||
// browser should take over.
|
|
||||||
if (!RESOURCES[key]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the URL is the index.html, perform an online-first request.
|
|
||||||
if (key == '/') {
|
|
||||||
return onlineFirst(event);
|
|
||||||
}
|
|
||||||
event.respondWith(caches.open(CACHE_NAME)
|
|
||||||
.then((cache) => {
|
|
||||||
return cache.match(event.request).then((response) => {
|
|
||||||
// Either respond with the cached resource, or perform a fetch and
|
|
||||||
// lazily populate the cache only if the resource was successfully fetched.
|
|
||||||
return response || fetch(event.request).then((response) => {
|
|
||||||
if (response && Boolean(response.ok)) {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
// SkipWaiting can be used to immediately activate a waiting service worker.
|
|
||||||
// This will also require a page refresh triggered by the main worker.
|
|
||||||
if (event.data === 'skipWaiting') {
|
|
||||||
self.skipWaiting();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.data === 'downloadOffline') {
|
|
||||||
downloadOffline();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Download offline will check the RESOURCES for all files not in the cache
|
|
||||||
// and populate them.
|
|
||||||
async function downloadOffline() {
|
|
||||||
var resources = [];
|
|
||||||
var contentCache = await caches.open(CACHE_NAME);
|
|
||||||
var currentContent = {};
|
|
||||||
for (var request of await contentCache.keys()) {
|
|
||||||
var key = request.url.substring(origin.length + 1);
|
|
||||||
if (key == "") {
|
|
||||||
key = "/";
|
|
||||||
}
|
|
||||||
currentContent[key] = true;
|
|
||||||
}
|
|
||||||
for (var resourceKey of Object.keys(RESOURCES)) {
|
|
||||||
if (!currentContent[resourceKey]) {
|
|
||||||
resources.push(resourceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contentCache.addAll(resources);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to download the resource online before falling back to
|
|
||||||
// the offline cache.
|
|
||||||
function onlineFirst(event) {
|
|
||||||
return event.respondWith(
|
|
||||||
fetch(event.request).then((response) => {
|
|
||||||
return caches.open(CACHE_NAME).then((cache) => {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
return caches.open(CACHE_NAME).then((cache) => {
|
|
||||||
return cache.match(event.request).then((response) => {
|
|
||||||
if (response != null) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
}
|
}
|
||||||
|
375
packages/flutter_tools/lib/src/web/file_generators/js/flutter.js
Normal file
375
packages/flutter_tools/lib/src/web/file_generators/js/flutter.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
// Copyright 2014 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.
|
||||||
|
|
||||||
|
if (!_flutter) {
|
||||||
|
var _flutter = {};
|
||||||
|
}
|
||||||
|
_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 || [
|
||||||
|
/\.dart\.js$/,
|
||||||
|
/^flutter_service_worker.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 (!("serviceWorker" in navigator) || settings == null) {
|
||||||
|
// In the future, settings = null -> uninstall service worker?
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("Service worker not supported (or configured).")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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(this._getNewServiceWorker)
|
||||||
|
.then(this._waitForServiceWorkerActivation);
|
||||||
|
|
||||||
|
// Timeout race promise
|
||||||
|
return timeout(
|
||||||
|
serviceWorkerActivation,
|
||||||
|
timeoutMillis,
|
||||||
|
"prepareServiceWorker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the latest service worker for the given `serviceWorkerRegistrationPromise`.
|
||||||
|
*
|
||||||
|
* This might return the current service worker, if there's no new service worker
|
||||||
|
* awaiting to be installed/updated.
|
||||||
|
*
|
||||||
|
* @param {Promise<ServiceWorkerRegistration>} serviceWorkerRegistrationPromise
|
||||||
|
* @returns {Promise<ServiceWorker>}
|
||||||
|
*/
|
||||||
|
async _getNewServiceWorker(serviceWorkerRegistrationPromise) {
|
||||||
|
const reg = await serviceWorkerRegistrationPromise;
|
||||||
|
|
||||||
|
if (!reg.active && (reg.installing || reg.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 reg.installing || reg.waiting;
|
||||||
|
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||||
|
// When the app updates the serviceWorkerVersion changes, so we
|
||||||
|
// need to ask the service worker to update.
|
||||||
|
return reg.update().then((newReg) => {
|
||||||
|
console.debug("Updating service worker.");
|
||||||
|
return newReg.installing || newReg.waiting || newReg.active;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.debug("Loading from existing service worker.");
|
||||||
|
return reg.active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Promise that resolves when the `latestServiceWorker` changes its
|
||||||
|
* state to "activated".
|
||||||
|
*
|
||||||
|
* @param {Promise<ServiceWorker>} latestServiceWorkerPromise
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async _waitForServiceWorkerActivation(latestServiceWorkerPromise) {
|
||||||
|
const serviceWorker = await latestServiceWorkerPromise;
|
||||||
|
|
||||||
|
if (!serviceWorker || serviceWorker.state == "activated") {
|
||||||
|
if (!serviceWorker) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("Cannot activate a null service worker!")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.debug("Service worker already active.");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 } =
|
||||||
|
options || {};
|
||||||
|
|
||||||
|
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const useCallback = typeof onEntrypointLoaded === "function";
|
||||||
|
|
||||||
|
if (!this._scriptLoaded) {
|
||||||
|
this._scriptLoaded = true;
|
||||||
|
const scriptTag = this._createScriptTag(entrypointUrl);
|
||||||
|
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) {
|
||||||
|
const scriptTag = document.createElement("script");
|
||||||
|
scriptTag.type = "application/javascript";
|
||||||
|
// 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();
|
||||||
|
})();
|
@ -0,0 +1,172 @@
|
|||||||
|
'use strict';
|
||||||
|
const MANIFEST = 'flutter-app-manifest';
|
||||||
|
const TEMP = 'flutter-temp-cache';
|
||||||
|
const CACHE_NAME = 'flutter-app-cache';
|
||||||
|
|
||||||
|
const RESOURCES = $$RESOURCES_MAP;
|
||||||
|
// The application shell files that are downloaded before a service worker can
|
||||||
|
// start.
|
||||||
|
const CORE = $$CORE_LIST;
|
||||||
|
|
||||||
|
// During install, the TEMP cache is populated with the application shell files.
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
return event.waitUntil(
|
||||||
|
caches.open(TEMP).then((cache) => {
|
||||||
|
return cache.addAll(
|
||||||
|
CORE.map((value) => new Request(value, {'cache': 'reload'})));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// During activate, the cache is populated with the temp files downloaded in
|
||||||
|
// install. If this service worker is upgrading from one with a saved
|
||||||
|
// MANIFEST, then use this to retain unchanged resource files.
|
||||||
|
self.addEventListener("activate", function(event) {
|
||||||
|
return event.waitUntil(async function() {
|
||||||
|
try {
|
||||||
|
var contentCache = await caches.open(CACHE_NAME);
|
||||||
|
var tempCache = await caches.open(TEMP);
|
||||||
|
var manifestCache = await caches.open(MANIFEST);
|
||||||
|
var manifest = await manifestCache.match('manifest');
|
||||||
|
// When there is no prior manifest, clear the entire cache.
|
||||||
|
if (!manifest) {
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
contentCache = await caches.open(CACHE_NAME);
|
||||||
|
for (var request of await tempCache.keys()) {
|
||||||
|
var response = await tempCache.match(request);
|
||||||
|
await contentCache.put(request, response);
|
||||||
|
}
|
||||||
|
await caches.delete(TEMP);
|
||||||
|
// Save the manifest to make future upgrades efficient.
|
||||||
|
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||||
|
// Claim client to enable caching on first launch
|
||||||
|
self.clients.claim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var oldManifest = await manifest.json();
|
||||||
|
var origin = self.location.origin;
|
||||||
|
for (var request of await contentCache.keys()) {
|
||||||
|
var key = request.url.substring(origin.length + 1);
|
||||||
|
if (key == "") {
|
||||||
|
key = "/";
|
||||||
|
}
|
||||||
|
// If a resource from the old manifest is not in the new cache, or if
|
||||||
|
// the MD5 sum has changed, delete it. Otherwise the resource is left
|
||||||
|
// in the cache and can be reused by the new service worker.
|
||||||
|
if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
|
||||||
|
await contentCache.delete(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Populate the cache with the app shell TEMP files, potentially overwriting
|
||||||
|
// cache files preserved above.
|
||||||
|
for (var request of await tempCache.keys()) {
|
||||||
|
var response = await tempCache.match(request);
|
||||||
|
await contentCache.put(request, response);
|
||||||
|
}
|
||||||
|
await caches.delete(TEMP);
|
||||||
|
// Save the manifest to make future upgrades efficient.
|
||||||
|
await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
|
||||||
|
// Claim client to enable caching on first launch
|
||||||
|
self.clients.claim();
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
// On an unhandled exception the state of the cache cannot be guaranteed.
|
||||||
|
console.error('Failed to upgrade service worker: ' + err);
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
await caches.delete(TEMP);
|
||||||
|
await caches.delete(MANIFEST);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
});
|
||||||
|
// The fetch handler redirects requests for RESOURCE files to the service
|
||||||
|
// worker cache.
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.method !== 'GET') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var origin = self.location.origin;
|
||||||
|
var key = event.request.url.substring(origin.length + 1);
|
||||||
|
// Redirect URLs to the index.html
|
||||||
|
if (key.indexOf('?v=') != -1) {
|
||||||
|
key = key.split('?v=')[0];
|
||||||
|
}
|
||||||
|
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
|
||||||
|
key = '/';
|
||||||
|
}
|
||||||
|
// If the URL is not the RESOURCE list then return to signal that the
|
||||||
|
// browser should take over.
|
||||||
|
if (!RESOURCES[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the URL is the index.html, perform an online-first request.
|
||||||
|
if (key == '/') {
|
||||||
|
return onlineFirst(event);
|
||||||
|
}
|
||||||
|
event.respondWith(caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
return cache.match(event.request).then((response) => {
|
||||||
|
// Either respond with the cached resource, or perform a fetch and
|
||||||
|
// lazily populate the cache only if the resource was successfully fetched.
|
||||||
|
return response || fetch(event.request).then((response) => {
|
||||||
|
if (response && Boolean(response.ok)) {
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
// SkipWaiting can be used to immediately activate a waiting service worker.
|
||||||
|
// This will also require a page refresh triggered by the main worker.
|
||||||
|
if (event.data === 'skipWaiting') {
|
||||||
|
self.skipWaiting();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.data === 'downloadOffline') {
|
||||||
|
downloadOffline();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Download offline will check the RESOURCES for all files not in the cache
|
||||||
|
// and populate them.
|
||||||
|
async function downloadOffline() {
|
||||||
|
var resources = [];
|
||||||
|
var contentCache = await caches.open(CACHE_NAME);
|
||||||
|
var currentContent = {};
|
||||||
|
for (var request of await contentCache.keys()) {
|
||||||
|
var key = request.url.substring(origin.length + 1);
|
||||||
|
if (key == "") {
|
||||||
|
key = "/";
|
||||||
|
}
|
||||||
|
currentContent[key] = true;
|
||||||
|
}
|
||||||
|
for (var resourceKey of Object.keys(RESOURCES)) {
|
||||||
|
if (!currentContent[resourceKey]) {
|
||||||
|
resources.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentCache.addAll(resources);
|
||||||
|
}
|
||||||
|
// Attempt to download the resource online before falling back to
|
||||||
|
// the offline cache.
|
||||||
|
function onlineFirst(event) {
|
||||||
|
return event.respondWith(
|
||||||
|
fetch(event.request).then((response) => {
|
||||||
|
return caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
return caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
return cache.match(event.request).then((response) => {
|
||||||
|
if (response != null) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
@ -785,19 +785,40 @@ void main() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
test('Generated service worker is empty with none-strategy', () {
|
test('Generated service worker is empty with none-strategy', () {
|
||||||
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'}, <String>[], serviceWorkerStrategy: ServiceWorkerStrategy.none);
|
final String fileGeneratorsPath =
|
||||||
|
environment.artifacts.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String result = generateServiceWorker(
|
||||||
|
fileGeneratorsPath,
|
||||||
|
<String, String>{'/foo': 'abcd'},
|
||||||
|
<String>[],
|
||||||
|
serviceWorkerStrategy: ServiceWorkerStrategy.none,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, '');
|
expect(result, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Generated service worker correctly inlines file hashes', () {
|
test('Generated service worker correctly inlines file hashes', () {
|
||||||
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'}, <String>[], serviceWorkerStrategy: ServiceWorkerStrategy.offlineFirst);
|
final String fileGeneratorsPath =
|
||||||
|
environment.artifacts.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String result = generateServiceWorker(
|
||||||
|
fileGeneratorsPath,
|
||||||
|
<String, String>{'/foo': 'abcd'},
|
||||||
|
<String>[],
|
||||||
|
serviceWorkerStrategy: ServiceWorkerStrategy.offlineFirst,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, contains('{\n "/foo": "abcd"\n};'));
|
expect(result, contains('{"/foo": "abcd"};'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Generated service worker includes core files', () {
|
test('Generated service worker includes core files', () {
|
||||||
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'}, <String>['foo', 'bar'], serviceWorkerStrategy: ServiceWorkerStrategy.offlineFirst);
|
final String fileGeneratorsPath =
|
||||||
|
environment.artifacts.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String result = generateServiceWorker(
|
||||||
|
fileGeneratorsPath,
|
||||||
|
<String, String>{'/foo': 'abcd'},
|
||||||
|
<String>['foo', 'bar'],
|
||||||
|
serviceWorkerStrategy: ServiceWorkerStrategy.offlineFirst,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, contains('"foo",\n"bar"'));
|
expect(result, contains('"foo",\n"bar"'));
|
||||||
});
|
});
|
||||||
@ -854,7 +875,10 @@ void main() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
test('flutter.js sanity checks', () {
|
test('flutter.js sanity checks', () {
|
||||||
final String flutterJsContents = flutter_js.generateFlutterJsFile();
|
final String fileGeneratorsPath = environment.artifacts
|
||||||
|
.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String flutterJsContents =
|
||||||
|
flutter_js.generateFlutterJsFile(fileGeneratorsPath);
|
||||||
expect(flutterJsContents, contains('"use strict";'));
|
expect(flutterJsContents, contains('"use strict";'));
|
||||||
expect(flutterJsContents, contains('main.dart.js'));
|
expect(flutterJsContents, contains('main.dart.js'));
|
||||||
expect(flutterJsContents, contains('flutter_service_worker.js?v='));
|
expect(flutterJsContents, contains('flutter_service_worker.js?v='));
|
||||||
@ -873,8 +897,14 @@ void main() {
|
|||||||
await WebBuiltInAssets(globals.fs, globals.cache, false).build(environment);
|
await WebBuiltInAssets(globals.fs, globals.cache, false).build(environment);
|
||||||
|
|
||||||
// No caching of source maps.
|
// No caching of source maps.
|
||||||
expect(environment.outputDir.childFile('flutter.js').readAsStringSync(),
|
final String fileGeneratorsPath = environment.artifacts
|
||||||
equals(flutter_js.generateFlutterJsFile()));
|
.getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String flutterJsContents =
|
||||||
|
flutter_js.generateFlutterJsFile(fileGeneratorsPath);
|
||||||
|
expect(
|
||||||
|
environment.outputDir.childFile('flutter.js').readAsStringSync(),
|
||||||
|
equals(flutterJsContents),
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test('wasm build copies and generates specific files', () => testbed.run(() async {
|
test('wasm build copies and generates specific files', () => testbed.run(() async {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
|
import 'package:flutter_tools/src/artifacts.dart';
|
||||||
import 'package:flutter_tools/src/web/file_generators/flutter_js.dart';
|
import 'package:flutter_tools/src/web/file_generators/flutter_js.dart';
|
||||||
|
|
||||||
import '../test_utils.dart';
|
import '../test_utils.dart';
|
||||||
@ -59,9 +60,13 @@ abstract class Project {
|
|||||||
}
|
}
|
||||||
deferredComponents?.setUpIn(dir);
|
deferredComponents?.setUpIn(dir);
|
||||||
|
|
||||||
|
final String fileGeneratorsPath =
|
||||||
|
Artifacts.test().getArtifactPath(Artifact.flutterToolsFileGenerators);
|
||||||
|
final String flutterJsContents = generateFlutterJsFile(fileGeneratorsPath);
|
||||||
|
|
||||||
// Setup for different flutter web initializations
|
// Setup for different flutter web initializations
|
||||||
writeFile(fileSystem.path.join(dir.path, 'web', 'index.html'), indexHtml);
|
writeFile(fileSystem.path.join(dir.path, 'web', 'index.html'), indexHtml);
|
||||||
writeFile(fileSystem.path.join(dir.path, 'web', 'flutter.js'), generateFlutterJsFile());
|
writeFile(fileSystem.path.join(dir.path, 'web', 'flutter.js'), flutterJsContents);
|
||||||
writeFile(fileSystem.path.join(dir.path, 'web', 'flutter_service_worker.js'), '');
|
writeFile(fileSystem.path.join(dir.path, 'web', 'flutter_service_worker.js'), '');
|
||||||
writePackages(dir.path);
|
writePackages(dir.path);
|
||||||
await getPackages(dir.path);
|
await getPackages(dir.path);
|
||||||
|
Loading…
Reference in New Issue
Block a user