diff --git a/src/Uno.Wasm.Bootstrap/Embedded/service-worker-classic.js b/src/Uno.Wasm.Bootstrap/Embedded/service-worker-classic.js new file mode 100644 index 000000000..0b1e37f39 --- /dev/null +++ b/src/Uno.Wasm.Bootstrap/Embedded/service-worker-classic.js @@ -0,0 +1,87 @@ +// As of Dec 2024, Firefox does not support ES6 modules in service workers, so we need to use importScripts +// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker#browser_compatibility +importScripts("$(REMOTE_WEBAPP_PATH)$(REMOTE_BASE_PATH)/uno-config-script.js"); + +if (config.environmentVariables["UNO_BOOTSTRAP_DEBUGGER_ENABLED"] !== "True") { + console.debug("[ServiceWorker] Initializing"); + let uno_enable_tracing = config.uno_enable_tracing; + + self.addEventListener('install', function (e) { + console.debug('[ServiceWorker] Installing offline worker'); + e.waitUntil( + caches.open('$(CACHE_KEY)').then(async function (cache) { + console.debug('[ServiceWorker] Caching app binaries and content'); + + // Add files one by one to avoid failed downloads to prevent the + // worker to fail installing. + for (var i = 0; i < config.offline_files.length; i++) { + try { + if (uno_enable_tracing) { + console.debug(`[ServiceWorker] cache ${key}`); + } + + await cache.add(config.offline_files[i]); + } + catch (e) { + console.debug(`[ServiceWorker] Failed to fetch ${config.offline_files[i]}`); + } + } + + // Add the runtime's own files to the cache. We cannot use the + // existing cached content from the runtime as the keys contain a + // hash we cannot reliably compute. + var c = await fetch("$(REMOTE_WEBAPP_PATH)_framework/blazor.boot.json"); + const monoConfigResources = (await c.json()).resources; + + var entries = { + ...(monoConfigResources.coreAssembly || {}) + , ...(monoConfigResources.assembly || {}) + , ...(monoConfigResources.lazyAssembly || {}) + , ...(monoConfigResources.jsModuleWorker || {}) + , ...(monoConfigResources.jsModuleGlobalization || {}) + , ...(monoConfigResources.jsModuleNative || {}) + , ...(monoConfigResources.jsModuleRuntime || {}) + , ...(monoConfigResources.wasmNative || {}) + , ...(monoConfigResources.icu || {}) + , ...(monoConfigResources.coreAssembly || {}) + }; + + for (var key in entries) { + var uri = `$(REMOTE_WEBAPP_PATH)_framework/${key}`; + + if (uno_enable_tracing) { + console.debug(`[ServiceWorker] cache ${uri}`); + } + + await cache.add(uri); + } + }) + ); + }); + + self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()); + }); + + self.addEventListener('fetch', event => { + event.respondWith(async function () { + try { + // Network first mode to get fresh content every time, then fallback to + // cache content if needed. + return await fetch(event.request); + } catch (err) { + return caches.match(event.request).then(response => { + return response || fetch(event.request); + }); + } + }()); + }); +} +else { + // In development, always fetch from the network and do not enable offline support. + // This is because caching would make development more difficult (changes would not + // be reflected on the first load after each change). + // It also breaks the hot reload feature because VS's browserlink is not always able to + // inject its own framework in the served scripts and pages. + self.addEventListener('fetch', () => { }); +} diff --git a/src/Uno.Wasm.Bootstrap/ShellTask.cs b/src/Uno.Wasm.Bootstrap/ShellTask.cs index e3e5c32e8..f4ccabe9f 100644 --- a/src/Uno.Wasm.Bootstrap/ShellTask.cs +++ b/src/Uno.Wasm.Bootstrap/ShellTask.cs @@ -152,10 +152,10 @@ public override bool Execute() ExtractAdditionalCSS(); RemoveDuplicateAssets(); GeneratePackageFolder(); - BuildServiceWorker(); + BuildServiceWorkers(); GenerateEmbeddedJs(); GenerateIndexHtml(); - GenerateConfig(); + GenerateConfigFiles(); RemoveDuplicateAssets(); } finally @@ -295,9 +295,17 @@ private void CopyContent() } } - private void BuildServiceWorker() + private void BuildServiceWorkers() { - using var resourceStream = GetType().Assembly.GetManifestResourceStream("Uno.Wasm.Bootstrap.v0.Embedded.service-worker.js"); + BuildServiceWorker(resource: "Uno.Wasm.Bootstrap.v0.Embedded.service-worker.js", outputFile: "service-worker.js"); + + // Case for browsers that do not support modules for service workers: Firefox for example + BuildServiceWorker(resource: "Uno.Wasm.Bootstrap.v0.Embedded.service-worker-classic.js", outputFile: "service-worker-classic.js"); + } + + private void BuildServiceWorker(string resource, string outputFile) + { + using var resourceStream = GetType().Assembly.GetManifestResourceStream(resource); using var reader = new StreamReader(resourceStream); var worker = TouchServiceWorker(reader.ReadToEnd()); @@ -309,7 +317,7 @@ private void BuildServiceWorker() memoryStream.Position = 0; - CopyStreamToOutput("service-worker.js", memoryStream, DeployMode.Root); + CopyStreamToOutput(outputFile, memoryStream, DeployMode.Root); } private void ExtractAdditionalJS() @@ -524,9 +532,16 @@ static string BuildDependencyPath(string dep, string baseLookup) ? $"\"{baseLookup}{Path.GetFileName(dep)}\"" : $"\"{baseLookup}{Path.GetFileNameWithoutExtension(dep)}\""; - private void GenerateConfig() + private void GenerateConfigFiles() + { + GenerateConfigFile("uno-config.js", isModule: true); + + GenerateConfigFile("uno-config-script.js", isModule: false); + } + + private void GenerateConfigFile(string fileName, bool isModule) { - var unoConfigJsPath = Path.Combine(_intermediateAssetsPath, "uno-config.js"); + var unoConfigJsPath = Path.Combine(_intermediateAssetsPath, fileName); using (var w = new StreamWriter(unoConfigJsPath, false, _utf8Encoding)) { @@ -535,7 +550,8 @@ private void GenerateConfig() .Where(d => !d.EndsWith("require.js") && !d.EndsWith("uno-bootstrap.js") - && !d.EndsWith("service-worker.js")) + && !d.EndsWith("service-worker.js") + && !d.EndsWith("service-worker-classic.js")) .Select(dep => BuildDependencyPath(dep, baseLookup))); var config = new StringBuilder(); @@ -546,7 +562,7 @@ private void GenerateConfig() .Select(f => f.GetMetadata("Link") .Replace("\\", "/") .Replace("wwwroot/", "")) - .Concat([$"uno-config.js", "_framework/dotnet.boot.js", "."]); + .Concat([fileName, "_framework/dotnet.boot.js", "."]); var offlineFiles = enablePWA ? string.Join(", ", sanitizedOfflineFiles.Select(f => $"\"{WebAppBasePath}{f}\"")) : ""; @@ -609,7 +625,9 @@ private void GenerateConfig() AddEnvironmentVariable("UNO_BOOTSTRAP_LOG_PROFILER_OPTIONS", LogProfilerOptions); } - config.AppendLine("export { config };"); + config.AppendLine(isModule ? + "export { config };" : + $"self.config = config;"); w.Write(config.ToString()); diff --git a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts index 9ec1b738f..ecd0695b9 100644 --- a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts +++ b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts @@ -258,7 +258,7 @@ namespace Uno.WebAssembly.Bootstrap { this._runMain(this._unoConfig.uno_main, []); - this.initializePWA(); + await this.initializePWA(); } catch (e) { console.error(e); @@ -511,7 +511,7 @@ namespace Uno.WebAssembly.Bootstrap { link.click(); } - private initializePWA() { + private async initializePWA(): Promise { if (typeof window === 'object' /* ENVIRONMENT_IS_WEB */) { @@ -523,15 +523,21 @@ namespace Uno.WebAssembly.Bootstrap { console.debug(`Registering service worker for ${_webAppBasePath}`); - navigator.serviceWorker - .register( - `${_webAppBasePath}service-worker.js`, { + try { + await navigator.serviceWorker.register(`${_webAppBasePath}service-worker.js`, { scope: _webAppBasePath, type: 'module' - }) - .then(function () { - console.debug('Service Worker Registered'); }); + console.debug('Service Worker Registered (module)'); + } catch (e) { + console.debug('Service Worker registration (module) failed.', e); + + console.debug('Falling back to classic service worker registration...'); + + await navigator.serviceWorker.register(`${_webAppBasePath}service-worker-classic.js`, {scope: _webAppBasePath}); + + console.debug('Service Worker Registered (classic)'); + } } } }