diff --git a/Blazor.PWA.MSBuild-NuGet-Icon.pdn b/Blazor.PWA.MSBuild-NuGet-Icon.pdn new file mode 100644 index 0000000..6b1e321 Binary files /dev/null and b/Blazor.PWA.MSBuild-NuGet-Icon.pdn differ diff --git a/Blazor.PWA.MSBuild-NuGet-Icon.png b/Blazor.PWA.MSBuild-NuGet-Icon.png new file mode 100644 index 0000000..f9d5884 Binary files /dev/null and b/Blazor.PWA.MSBuild-NuGet-Icon.png differ diff --git a/Blazor.PWA.MSBuild.Tasks/Blazor.PWA.MSBuild.Tasks.csproj b/Blazor.PWA.MSBuild.Tasks/Blazor.PWA.MSBuild.Tasks.csproj new file mode 100644 index 0000000..6ae2c26 --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/Blazor.PWA.MSBuild.Tasks.csproj @@ -0,0 +1,75 @@ + + + + netstandard1.6 + + true + + tasks + true + Mister Magoo + 2019 SQL-MisterMagoo + true + The easiest way to turn your Client Side Blazor application into a PWA with offline capabilities. +Add this package to your build process and it will generate the files you need to become PWA compatible. +This does not make your application ready for distribution through "App Stores" - it just provides basic PWA functionality. + Blazor.PWA.MSBuild + LICENSE.txt + + false + https://github.com/SQL-MisterMagoo/Blazor.PWA.MSBuild/src/Blazor.PWA.MSBuild + Blazor,Build,MSBuild,PWA,Manifest,ServiceWorker,C#,DotNET,Web,Client + Blazor.PWA.MSBuild + https://github.com/SQL-MisterMagoo/Blazor.PWA.MSBuild + https://github.com/SQL-MisterMagoo/Blazor.PWA.MSBuild/src/Blazor.PWA.MSBuild/Blazor.PWA.MSBuild-NuGet-Icon.png + git + This is a beta release of a very basic PWA build target. +Just by including this in the build, it can generate the basic requirements for an installable PWA for client side Blazor. +The result is not an App Store package, it is simply the basic requirements for PWA. +I will add more network caching strategies, but for now it has just one - cache all local assets. + + + + + + + + + True + + + + + + + build\ + True + + + build\ + True + + + build\ + True + + + build\ + True + + + True + Templates\ + + + True + Templates\ + + + + + + + + + diff --git a/Blazor.PWA.MSBuild.Tasks/Directory.Build.props b/Blazor.PWA.MSBuild.Tasks/Directory.Build.props new file mode 100644 index 0000000..6d8bf77 --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/Directory.Build.props @@ -0,0 +1,7 @@ + + + 0.0.1 + beta$([System.DateTime]::Now.ToString("yyyyMMdd-HHmmss")) + beta$([System.DateTime]::Now.ToString("yyyyMMdd-HH")) + + diff --git a/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-192x192.png b/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-192x192.png new file mode 100644 index 0000000..a55dcd1 Binary files /dev/null and b/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-192x192.png differ diff --git a/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-512x512.png b/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-512x512.png new file mode 100644 index 0000000..b07b517 Binary files /dev/null and b/Blazor.PWA.MSBuild.Tasks/Templates/Images/default-icon-512x512.png differ diff --git a/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_fetch-offline-first.template.js b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_fetch-offline-first.template.js new file mode 100644 index 0000000..7a4948d --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_fetch-offline-first.template.js @@ -0,0 +1,30 @@ +self.addEventListener(networkFetchEvent, event => { + const requestUrl = new URL(event.request.url); + if (requestUrl.origin === location.origin) { + if (requestUrl.pathname === baseURL) { + event.respondWith(caches.match(indexURL)); + return; + } + } + event.respondWith( + caches.match(event.request) + .then(response => { + if (response) { + return response; + } + return fetch(event.request) + .then(response => { + if (response.ok) { + if (requestUrl.origin === location.origin) { + caches.open(staticCacheName).then(cache => { + cache.put(event.request.url, response); + }); + } + } + return response.clone(); + }); + }).catch(error => { + console.error(error); + }) + ); +}); diff --git a/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_install.template.js b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_install.template.js new file mode 100644 index 0000000..9c276b1 --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_install.template.js @@ -0,0 +1,23 @@ +// * listen for the install event and pre-cache anything in filesToCache * // +self.addEventListener(swInstallEvent, event => { + self.skipWaiting(); + event.waitUntil( + caches.open(staticCacheName) + .then(cache => { + return cache.addAll(requiredFiles); + }) + ); +}); +self.addEventListener(swActivateEvent, function (event) { + event.waitUntil( + caches.keys().then(function (cacheNames) { + return Promise.all( + cacheNames.map(function (cacheName) { + if (staticCacheName !== cacheName && cacheName.startsWith(staticCachePrefix)) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); \ No newline at end of file diff --git a/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_register.template.js b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_register.template.js new file mode 100644 index 0000000..05d7f6a --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/Templates/ServiceWorker/sw_register.template.js @@ -0,0 +1,93 @@ +window.updateAvailable = new Promise(function (resolve, reject) { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register(serviceWorkerFileName) + .then(function (registration) { + console.log('Registration successful, scope is:', registration.scope); + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + switch (installingWorker.state) { + case swInstalledEvent: + if (navigator.serviceWorker.controller) { + resolve(true); + } else { + resolve(false); + } + break; + default: + } + }; + }; + }) + .catch(error => + console.log('Service worker registration failed, error:', error)); + } +}); +window['updateAvailable'] + .then(isAvailable => { + if (isAvailable) { + alert("Update available. Reload the page when convenient."); + } + }); + +window.addEventListener('beforeinstallprompt', function (e) { + // Prevent Chrome 67 and earlier from automatically showing the prompt + e.preventDefault(); + // Stash the event so it can be triggered later. + window.PWADeferredPrompt = e; + + showAddToHomeScreen(); + +}); + +function showAddToHomeScreen() { + var pwaInstallPrompt = document.createElement('div'); + var pwaInstallButton = document.createElement('button'); + var pwaCancelButton = document.createElement('button'); + + pwaInstallPrompt.id = 'pwa-install-prompt'; + pwaInstallPrompt.style.position = 'absolute'; + pwaInstallPrompt.style.bottom = '0'; + pwaInstallPrompt.style.display = 'flex'; + pwaInstallPrompt.style.width = '100vw'; + pwaInstallPrompt.style.backgroundColor='darkslategrey'; + pwaInstallPrompt.style.color='white'; + pwaInstallPrompt.style.fontSize='2rem'; + + pwaInstallButton.style.marginLeft='auto'; + pwaInstallButton.style.width='4em'; + pwaInstallButton.style.backgroundColor='green'; + pwaInstallButton.style.color='white'; + + pwaCancelButton.style.marginLeft='0.3rem'; + pwaCancelButton.style.backgroundColor='darkslategray'; + pwaCancelButton.style.color='white'; + + pwaInstallPrompt.innerText = 'Add to your homescreen!'; + pwaInstallButton.innerText = 'OK'; + pwaCancelButton.innerText = 'Ignore'; + + pwaInstallPrompt.appendChild(pwaInstallButton); + pwaInstallPrompt.appendChild(pwaCancelButton); + document.body.appendChild(pwaInstallPrompt); + + pwaInstallButton.addEventListener('click', addToHomeScreen); + pwaCancelButton.addEventListener('click', hideAddToHomeScreen); + setTimeout(hideAddToHomeScreen, 10000); +} + +function hideAddToHomeScreen() { + var pwa = document.getElementById('pwa-install-prompt'); + if (pwa) document.body.removeChild(pwa); +} + +function addToHomeScreen(s, e) { + hideAddToHomeScreen(); + if (window.PWADeferredPrompt) { + window.PWADeferredPrompt.prompt(); + window.PWADeferredPrompt.userChoice + .then(function (choiceResult) { + window.PWADeferredPrompt = null; + }); + } +} diff --git a/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.Manifest.targets b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.Manifest.targets new file mode 100644 index 0000000..dfa2bec --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.Manifest.targets @@ -0,0 +1,86 @@ + + + + + + + + + + $(SolutionName) + $(ProjectName) + + + / + + + standalone + + + $(MSBuildThisFileDirectory)..\Templates\ + $(ManifestTemplatePath)default-icon-192x192.png + $(ManifestTemplatePath)default-icon-512x512.png + + + + + + + + + 192x192 + + + 512x512 + + + + + + + + + + + 192x192 + + + 512x512 + + + + + + + + + + + + $(WWWRoot)$(ServiceWorkerIndexUrl) + $([System.IO.File]::ReadAllText($(IndexFile))) + + + + + + + \ No newline at end of file diff --git a/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorker.targets b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorker.targets new file mode 100644 index 0000000..c57291a --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorker.targets @@ -0,0 +1,106 @@ + + + + + + + + + + + + + install + + + installed + + + activate + + + fetch + + + + + $(OutputPath)dist\**\*.*; + $(WWWRoot)**\*.*; + + + + + $(ServiceWorkerPreCacheExcludeFiles); + $(OutputPath)dist\_content\**\*.*; + $(WWWRoot)**\.*.*; + + + + + $(ServiceWorkerPreCacheExcludeReleaseFiles); + $(OutputPath)dist\**\*.pdb; + + + + + $(WWWRoot)**\$(ServiceWorkerRegisterFileName); + $(WWWRoot)**\$(ManifestFileName); + + + + + const baseURL = '$(ServiceWorkerBaseURL)'%3B; + const indexURL = '$(ServiceWorkerBaseUrl)$(ServiceWorkerIndexURL)'%3B; + const networkFetchEvent = '$(ServiceWorkerNetworkFetchEvent)'%3B; + const swInstallEvent = '$(ServiceWorkerInstallEvent)'%3B; + const swInstalledEvent = '$(ServiceWorkerInstalledEvent)'%3B; + const swActivateEvent = '$(ServiceWorkerActivateEvent)'%3B; + const staticCachePrefix = '$(ServiceWorkerCacheName)-v'%3B; + const staticCacheName = '$(ServiceWorkerCacheName)-v$(ServiceWorkerCacheVersion)'%3B; + + + + + + + $(MSBuildThisFileDirectory)..\Templates\ServiceWorker\ + + $(ServiceWorkerTemplatePath)sw_$(ServiceWorkerInstallEvent).template.js + + $(ServiceWorkerTemplatePath)sw_$(ServiceWorkerNetworkFetchEvent)-$(ServiceWorkerPattern).template.js + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorkerRegister.targets b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorkerRegister.targets new file mode 100644 index 0000000..24c02db --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.ServiceWorkerRegister.targets @@ -0,0 +1,51 @@ + + + + + + + + + + + $(WWWRoot)$(ServiceWorkerIndexUrl) + $([System.IO.File]::ReadAllText($(IndexFile))) + + + + + + + + + $(MSBuildThisFileDirectory)..\Templates\ServiceWorker\ + + $(ServiceWorkerRegisterTemplatePath)sw_register.template.js + + + + + installed + + + const serviceWorkerFileName = '$(ServiceWorkerBaseURL)$(ServiceWorkerFileName)'%3B; + const swInstalledEvent = '$(ServiceWorkerInstalledEvent)'%3B; + const staticCachePrefix = '$(ServiceWorkerCacheName)-v'%3B; + + + + + + + + + \ No newline at end of file diff --git a/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.targets b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.targets new file mode 100644 index 0000000..bc1fce3 --- /dev/null +++ b/Blazor.PWA.MSBuild.Tasks/build/Blazor.PWA.MSBuild.targets @@ -0,0 +1,63 @@ + + + + + + + + + ServiceWorker.js + ServiceWorkerRegister.js + + + + offline-first + + offline-first + + + blazor-cache + 1 + + + / + + + index.html + + + + + + manifest.json + + + $(ServiceWorkerForce) + + + + + $(ServiceWorkerForce) + + + + AfterBuild + + + + + $(MsBuildProjectDirectory)\wwwroot\ + + + + + + + + + + + \ No newline at end of file diff --git a/Blazor.PWA.MSBuild.sln b/Blazor.PWA.MSBuild.sln new file mode 100644 index 0000000..a9531bd --- /dev/null +++ b/Blazor.PWA.MSBuild.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.89 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.PWA.MSBuild.Tasks", "Blazor.PWA.MSBuild.Tasks\Blazor.PWA.MSBuild.Tasks.csproj", "{27D84A0F-8F21-4E52-9318-E4403A87E11E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BCE600C8-35D7-4AA4-9FFB-156E4526097E}" + ProjectSection(SolutionItems) = preProject + LICENSE.txt = LICENSE.txt + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {27D84A0F-8F21-4E52-9318-E4403A87E11E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27D84A0F-8F21-4E52-9318-E4403A87E11E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27D84A0F-8F21-4E52-9318-E4403A87E11E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27D84A0F-8F21-4E52-9318-E4403A87E11E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8F44D916-3E04-4715-82E5-7B961362548D} + EndGlobalSection +EndGlobal diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b64e991 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Mister Magoo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..256f8c2 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Blazor PWA Builder - MSBuild + +The easiest way to turn your Client Side Blazor project into a Progressive Web App (PWA). + +This project, when added to your build process, will generate the required files to enable basic PWA abilities for your project. + +It generates a manifest.json, a Service Worker with pre-caching of all required files and an installer for the Service Worker. + +It currently includes a simple banner to notify the user that your application can be installed, and a simple alert to notify users when your application has been updated. + +This is to be considered a Beta release - I am looking for feedback/issues/requests. + +## Installation + +Install the nuget Blazor.PWA.MsBuild + +#### Package Manager: +`Install-Package Blazor.PWA.MSBuild -Version 0.0.1-beta20190808-10` + +#### .NET Cli: +`dotnet add package Blazor.PWA.MSBuild --version 0.0.1-beta20190808-10` + +#### Package Reference +`` + +## Configuration + +Required configuration: none *. + +**Really! try it!* + +This PWA builder is constructed using MSBuild targets, which get installed in your .nuget cache. + +You are free to inspect those targets and customise any part of the build by adding Properties to your csproj, or to a **.props** file in the source tree. + +For example, by default, the PWA builder will only generate the PWA files once, which means, should you want to, you can add it to your project, build once - you will have the required files for a PWA and will be free to modify them to suit. + +Should you want to re-generate the PWA files every build, you can add this Property to your **csproj** + +**`true`** + +When you want to publish an update to your app, you may need to supply a new browser cache version - if you don't change anything in the Service Worker the end user will not use your new code! + +*Note: PWAs update when they detect a change in the Service Worker code - this cache version number is used in the Service Worker, so updating the cache version number will trigger an update of the PWA for the end user. It's also important as the cache is where your code is stored - if you don't update the cache version, it will not refresh with your new code* + +**`2`** + +The web manifest has properties for the application name, which are taken, by default, from your project and solution names, but you can override them + +**`My Project`** + +**`My Really Great Project`** + +There are dozens of Properties in the *targets* files supplied by this package - you *could* customise them all, but you probably don't need to, so proceed with caution. + +## Roadmap + +- [ ] At the moment, there is only one choice for caching strategy - Cache First/Network Fallback - I will add more (https://developers.google.com/web/ilt/pwa/introduction-to-progressive-web-app-architectures#caching_strategies_supported_by_sw-toolbox) +- [ ] The current methods for alerting the user are semi-hard coded (you can adjust them manually after generation) - this will change to allow hooks/callbacks into Blazor via project properties +- [ ] Document all of the configuration Properties (they all have comments in the code - so you are able to understand their purpose without documentation...) +- [ ] Bug fixes + +## Contribute + +Please feel free to create issues, ask questions and submit Pull Requests. \ No newline at end of file