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 3050f941b..4251ae12a 100644 --- a/src/Uno.Wasm.Bootstrap/ShellTask.cs +++ b/src/Uno.Wasm.Bootstrap/ShellTask.cs @@ -150,10 +150,10 @@ public override bool Execute() ExtractAdditionalCSS(); RemoveDuplicateAssets(); GeneratePackageFolder(); - BuildServiceWorker(); + BuildServiceWorkers(); GenerateEmbeddedJs(); GenerateIndexHtml(); - GenerateConfig(); + GenerateConfigFiles(); RemoveDuplicateAssets(); } finally @@ -293,9 +293,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()); @@ -307,7 +315,7 @@ private void BuildServiceWorker() memoryStream.Position = 0; - CopyStreamToOutput("service-worker.js", memoryStream, DeployMode.Root); + CopyStreamToOutput(outputFile, memoryStream, DeployMode.Root); } private void ExtractAdditionalJS() @@ -522,9 +530,18 @@ 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 self = isModule ? "" : "self."; + + var unoConfigJsPath = Path.Combine(_intermediateAssetsPath, fileName); using (var w = new StreamWriter(unoConfigJsPath, false, _utf8Encoding)) { @@ -533,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(); @@ -544,7 +562,7 @@ private void GenerateConfig() .Select(f => f.GetMetadata("Link") .Replace("\\", "/") .Replace("wwwroot/", "")) - .Concat([$"uno-config.js", "_framework/blazor.boot.json", "."]); + .Concat([fileName, "_framework/blazor.boot.json", "."]); var offlineFiles = enablePWA ? string.Join(", ", sanitizedOfflineFiles.Select(f => $"\"{WebAppBasePath}{f}\"")) : ""; @@ -554,27 +572,36 @@ private void GenerateConfig() var runtimeOptionsSet = string.Join(",", (RuntimeOptions?.Split(' ') ?? []).Select(f => $"\'{f}\'")); - config.AppendLine($"let config = {{}};"); - config.AppendLine($"config.uno_remote_managedpath = \"_framework\";"); - config.AppendLine($"config.uno_app_base = \"{WebAppBasePath}{PackageAssetsFolder}\";"); - config.AppendLine($"config.uno_dependencies = [{dependencies}];"); - config.AppendLine($"config.uno_runtime_options = [{runtimeOptionsSet}];"); - config.AppendLine($"config.enable_pwa = {enablePWA.ToString().ToLowerInvariant()};"); - config.AppendLine($"config.offline_files = ['{WebAppBasePath}', {offlineFiles}];"); - config.AppendLine($"config.uno_shell_mode = \"{_shellMode}\";"); - config.AppendLine($"config.uno_debugging_enabled = {(!Optimize).ToString().ToLowerInvariant()};"); - config.AppendLine($"config.uno_enable_tracing = {EnableTracing.ToString().ToLowerInvariant()};"); - config.AppendLine($"config.uno_load_all_satellite_resources = {LoadAllSatelliteResources.ToString().ToLowerInvariant()};"); - config.AppendLine($"config.emcc_exported_runtime_methods = [{emccExportedRuntimeMethodsParams}];"); + + if (isModule) + { + config.AppendLine($"let config = {{}};"); + } + else + { + config.AppendLine($"{self}config = {{}};"); + } + + config.AppendLine($"{self}config.uno_remote_managedpath = \"_framework\";"); + config.AppendLine($"{self}config.uno_app_base = \"{WebAppBasePath}{PackageAssetsFolder}\";"); + config.AppendLine($"{self}config.uno_dependencies = [{dependencies}];"); + config.AppendLine($"{self}config.uno_runtime_options = [{runtimeOptionsSet}];"); + config.AppendLine($"{self}config.enable_pwa = {enablePWA.ToString().ToLowerInvariant()};"); + config.AppendLine($"{self}config.offline_files = ['{WebAppBasePath}', {offlineFiles}];"); + config.AppendLine($"{self}config.uno_shell_mode = \"{_shellMode}\";"); + config.AppendLine($"{self}config.uno_debugging_enabled = {(!Optimize).ToString().ToLowerInvariant()};"); + config.AppendLine($"{self}config.uno_enable_tracing = {EnableTracing.ToString().ToLowerInvariant()};"); + config.AppendLine($"{self}config.uno_load_all_satellite_resources = {LoadAllSatelliteResources.ToString().ToLowerInvariant()};"); + config.AppendLine($"{self}config.emcc_exported_runtime_methods = [{emccExportedRuntimeMethodsParams}];"); if (GenerateAOTProfile) { - config.AppendLine($"config.generate_aot_profile = true;"); + config.AppendLine($"{self}config.generate_aot_profile = true;"); } - config.AppendLine($"config.environmentVariables = config.environmentVariables || {{}};"); + config.AppendLine($"{self}config.environmentVariables = {self}config.environmentVariables || {{}};"); - void AddEnvironmentVariable(string name, string value) => config.AppendLine($"config.environmentVariables[\"{name}\"] = \"{value}\";"); + void AddEnvironmentVariable(string name, string value) => config.AppendLine($"{self}config.environmentVariables[\"{name}\"] = \"{value}\";"); if (MonoEnvironment != null) { @@ -606,7 +633,10 @@ private void GenerateConfig() AddEnvironmentVariable("UNO_BOOTSTRAP_LOG_PROFILER_OPTIONS", LogProfilerOptions); } - config.AppendLine("export { config };"); + if (isModule) + { + config.AppendLine("export { config };"); + } w.Write(config.ToString()); @@ -622,6 +652,7 @@ private void GenerateConfig() } } + private void GenerateIndexHtml() { if (_shellMode != ShellMode.Browser) diff --git a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts index af2a3d3c5..97b125b09 100644 --- a/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts +++ b/src/Uno.Wasm.Bootstrap/ts/Uno/WebAssembly/Bootstrapper.ts @@ -259,7 +259,7 @@ namespace Uno.WebAssembly.Bootstrap { this._runMain(this._unoConfig.uno_main, []); - this.initializePWA(); + await this.initializePWA(); } catch (e) { console.error(e); @@ -510,7 +510,7 @@ namespace Uno.WebAssembly.Bootstrap { link.click(); } - private initializePWA() { + private async initializePWA(): Promise { if (typeof window === 'object' /* ENVIRONMENT_IS_WEB */) { @@ -522,15 +522,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)'); + } } } }