PWA template (#18878)
* Add service worker * Add manifest * Bring back BaselineTest.cs * Add baselines for blazorwasm templates * Add publishing test for PWA template * Baseline fixes * Fix baseline test logic to allow for multi-project outputs * Remove non-blazorwasm baselines, since this branch now only covers blazorwasm * Add test for PWA publish output * Beginning generation of assets manifest * Generate assets manifest including blazor outputs * Tweaks * Write assets manifest in JSON form * Publish service worker * Better API * More resilience * Better API again * Make ComputeBlazorAssetsManifestItems public as people will need to customize the list * Exclude service worker files from assets manifest * Use web standard format for hash * Update project template * In assets manifest, only include items being published * Renames * Compute default assets manifest version by combining hashes * Emit sw manifest in .js form * Update service worker in project * Actually isolate browser instances when requested during E2E tests * E2E test for published PWA operating offline * Fix SWAM path in template * Clarify targets
This commit is contained in:
parent
52f46367b0
commit
04b4602c2b
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization.Json;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Build
|
||||||
|
{
|
||||||
|
public class GenerateServiceWorkerAssetsManifest : Task
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ITaskItem[] AssetsWithHashes { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string OutputPath { get; set; }
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
using var fileStream = File.Create(OutputPath);
|
||||||
|
WriteFile(fileStream);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void WriteFile(Stream stream)
|
||||||
|
{
|
||||||
|
var data = new AssetsManifestFile
|
||||||
|
{
|
||||||
|
version = Version,
|
||||||
|
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
|
||||||
|
{
|
||||||
|
url = item.GetMetadata("AssetUrl"),
|
||||||
|
hash = $"sha256-{item.GetMetadata("FileHash")}",
|
||||||
|
}).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true);
|
||||||
|
streamWriter.Write("self.assetsManifest = ");
|
||||||
|
streamWriter.Flush();
|
||||||
|
|
||||||
|
using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true);
|
||||||
|
new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data);
|
||||||
|
jsonWriter.Flush();
|
||||||
|
|
||||||
|
streamWriter.WriteLine(";");
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable IDE1006 // Naming Styles
|
||||||
|
public class AssetsManifestFile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a version string.
|
||||||
|
/// </summary>
|
||||||
|
public string version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes.
|
||||||
|
/// </summary>
|
||||||
|
public AssetsManifestFileEntry[] assets { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AssetsManifestFileEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the asset URL. Normally this will be relative to the application's base href.
|
||||||
|
/// </summary>
|
||||||
|
public string url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value.
|
||||||
|
/// </summary>
|
||||||
|
public string hash { get; set; }
|
||||||
|
}
|
||||||
|
#pragma warning restore IDE1006 // Naming Styles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<Import Project="Blazor.MonoRuntime.targets" />
|
<Import Project="Blazor.MonoRuntime.targets" />
|
||||||
<Import Project="Publish.targets" />
|
<Import Project="Publish.targets" />
|
||||||
<Import Project="StaticWebAssets.targets" />
|
<Import Project="StaticWebAssets.targets" />
|
||||||
|
<Import Project="ServiceWorkerAssetsManifest.targets" />
|
||||||
|
|
||||||
<Target Name="GenerateBlazorMetadataFile"
|
<Target Name="GenerateBlazorMetadataFile"
|
||||||
BeforeTargets="GetCopyToOutputDirectoryItems">
|
BeforeTargets="GetCopyToOutputDirectoryItems">
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<Target
|
<Target
|
||||||
Name="_BlazorCopyFilesToOutputDirectory"
|
Name="_BlazorCopyFilesToOutputDirectory"
|
||||||
DependsOnTargets="PrepareBlazorOutputs"
|
DependsOnTargets="PrepareBlazorOutputs;$(_BlazorCopyFilesToOutputDirectoryDependsOn)"
|
||||||
AfterTargets="CopyFilesToOutputDirectory"
|
AfterTargets="CopyFilesToOutputDirectory"
|
||||||
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
|
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
|
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
|
||||||
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
|
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
|
||||||
-->
|
-->
|
||||||
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
|
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
|
||||||
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
|
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
|
||||||
|
|
||||||
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
|
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<Project>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_BlazorCopyFilesToOutputDirectoryDependsOn>
|
||||||
|
$(_BlazorCopyFilesToOutputDirectoryDependsOn);
|
||||||
|
_ComputeServiceWorkerAssetsManifestInputs;
|
||||||
|
_WriteServiceWorkerAssetsManifest;
|
||||||
|
</_BlazorCopyFilesToOutputDirectoryDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
|
DependsOnTargets="PrepareBlazorOutputs">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(BlazorIntermediateOutputPath)serviceworkerassets.js</_ServiceWorkerAssetsManifestIntermediateOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Include _framework/* content -->
|
||||||
|
<ServiceWorkerAssetsManifestItem
|
||||||
|
Include="@(BlazorOutputWithTargetPath)"
|
||||||
|
Condition="$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').StartsWith('dist/'))">
|
||||||
|
<AssetUrl>$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').Substring(5))</AssetUrl>
|
||||||
|
</ServiceWorkerAssetsManifestItem>
|
||||||
|
|
||||||
|
<!-- Include content from wwwroot -->
|
||||||
|
<ServiceWorkerAssetsManifestItem
|
||||||
|
Include="@(ContentWithTargetPath)"
|
||||||
|
Condition="
|
||||||
|
('%(ContentWithTargetPath.CopyToPublishDirectory)' == 'Always' OR '%(ContentWithTargetPath.CopyToPublishDirectory)' == 'PreserveNewest')
|
||||||
|
AND $([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').StartsWith('wwwroot/'))">
|
||||||
|
<AssetUrl>$([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').Substring(8))</AssetUrl>
|
||||||
|
</ServiceWorkerAssetsManifestItem>
|
||||||
|
|
||||||
|
<!-- Include SWA from references -->
|
||||||
|
<ServiceWorkerAssetsManifestItem
|
||||||
|
Include="@(StaticWebAsset)"
|
||||||
|
Condition="'%(StaticWebAsset.SourceType)' != ''">
|
||||||
|
<AssetUrl>%(StaticWebAsset.BasePath)/%(StaticWebAsset.RelativePath)</AssetUrl>
|
||||||
|
</ServiceWorkerAssetsManifestItem>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(BlazorTasksPath)" />
|
||||||
|
|
||||||
|
<Target Name="_WriteServiceWorkerAssetsManifest"
|
||||||
|
Inputs="@(ServiceWorkerAssetsManifestItem)"
|
||||||
|
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
|
||||||
|
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes; _ComputeDefaultServiceWorkerAssetsManifestVersion">
|
||||||
|
|
||||||
|
<GenerateServiceWorkerAssetsManifest
|
||||||
|
Version="$(ServiceWorkerAssetsManifestVersion)"
|
||||||
|
AssetsWithHashes="@(_ServiceWorkerAssetsManifestItemWithHash)"
|
||||||
|
OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<BlazorOutputWithTargetPath
|
||||||
|
Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
|
||||||
|
TargetOutputPath="$(BaseBlazorDistPath)$(ServiceWorkerAssetsManifest)" />
|
||||||
|
|
||||||
|
<FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_ComputeServiceWorkerAssetsManifestFileHashes">
|
||||||
|
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
|
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestItemWithHash" />
|
||||||
|
</GetFileHash>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If no ServiceWorkerAssetsManifestVersion was specified, we compute a default value by combining all the asset hashes.
|
||||||
|
This is useful because then clients will only have to repopulate caches if the contents have changed.
|
||||||
|
-->
|
||||||
|
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifestVersion)' == ''">
|
||||||
|
<PropertyGroup>
|
||||||
|
<_CombinedHashIntermediatePath>$(BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<WriteLinesToFile
|
||||||
|
File="$(_CombinedHashIntermediatePath)"
|
||||||
|
Lines="@(_ServiceWorkerAssetsManifestItemWithHash->'%(FileHash)')"
|
||||||
|
Overwrite="true" />
|
||||||
|
|
||||||
|
<GetFileHash Files="$(_CombinedHashIntermediatePath)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
|
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestCombinedHash" />
|
||||||
|
</GetFileHash>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ServiceWorkerAssetsManifestVersion>$([System.String]::Copy('%(_ServiceWorkerAssetsManifestCombinedHash.FileHash)').Substring(0, 8))</ServiceWorkerAssetsManifestVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
@ECHO OFF
|
@ECHO OFF
|
||||||
|
|
||||||
%~dp0..\..\startvs.cmd %~dp0Components.sln
|
%~dp0..\..\startvs.cmd %~dp0Blazor.sln
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<RazorLangVersion>3.0</RazorLangVersion>
|
<RazorLangVersion>3.0</RazorLangVersion>
|
||||||
|
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -11,10 +12,22 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="${MicrosoftAspNetCoreBlazorDevServerPackageVersion}" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="${MicrosoftAspNetCoreBlazorDevServerPackageVersion}" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="${MicrosoftAspNetCoreBlazorHttpClientPackageVersion}" />
|
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="${MicrosoftAspNetCoreBlazorHttpClientPackageVersion}" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--#if Hosted -->
|
<!--#if Hosted -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Shared\BlazorWasm-CSharp.Shared.csproj" />
|
<ProjectReference Include="..\Shared\BlazorWasm-CSharp.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!--#endif -->
|
|
||||||
|
|
||||||
|
<!--#endif -->
|
||||||
|
<!--#if PWA -->
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- When publishing, swap service-worker.published.js in place of service-worker.js -->
|
||||||
|
<Content Update="wwwroot\service-worker*.js" CopyToPublishDirectory="false" />
|
||||||
|
<ContentWithTargetPath Include="wwwroot\service-worker.published.js">
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
<TargetPath>wwwroot\service-worker.js</TargetPath>
|
||||||
|
</ContentWithTargetPath>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--#endif -->
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@
|
||||||
"longName": "no-restore",
|
"longName": "no-restore",
|
||||||
"shortName": ""
|
"shortName": ""
|
||||||
},
|
},
|
||||||
"Hosted": {
|
"Hosted": {
|
||||||
"longName": "hosted"
|
"longName": "hosted"
|
||||||
},
|
},
|
||||||
|
"PWA": {
|
||||||
|
"longName": "pwa"
|
||||||
|
},
|
||||||
"Framework": {
|
"Framework": {
|
||||||
"longName": "framework"
|
"longName": "framework"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,14 @@
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"*.sln"
|
"*.sln"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "(!PWA)",
|
||||||
|
"exclude": [
|
||||||
|
"Client/wwwroot/service-worker*.js",
|
||||||
|
"Client/wwwroot/manifest.json",
|
||||||
|
"Client/wwwroot/icon-512.png"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +155,12 @@
|
||||||
"fallbackVariableName": "HttpsPortGenerated"
|
"fallbackVariableName": "HttpsPortGenerated"
|
||||||
},
|
},
|
||||||
"replaces": "44300"
|
"replaces": "44300"
|
||||||
|
},
|
||||||
|
"PWA": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "bool",
|
||||||
|
"defaultValue": "false",
|
||||||
|
"description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@
|
||||||
"text": "ASP.NET Core _hosted"
|
"text": "ASP.NET Core _hosted"
|
||||||
},
|
},
|
||||||
"isVisible": "true"
|
"isVisible": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PWA",
|
||||||
|
"name": {
|
||||||
|
"text": "_Progressive Web Application"
|
||||||
|
},
|
||||||
|
"isVisible": "true"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
|
|
@ -8,6 +8,9 @@
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="css/site.css" rel="stylesheet" />
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
|
<!--#if PWA -->
|
||||||
|
<link href="manifest.json" rel="manifest" />
|
||||||
|
<!--#endif -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -19,6 +22,9 @@
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</div>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
<!--#if PWA -->
|
||||||
|
<script>navigator.serviceWorker.register('service-worker.js');</script>
|
||||||
|
<!--#endif -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "BlazorWasm-CSharp",
|
||||||
|
"short_name": "BlazorWasm-CSharp",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#03173d",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
// 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).
|
||||||
|
self.addEventListener('fetch', () => { });
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Caution! Be sure you understand the caveats before publishing an application with
|
||||||
|
// offline support. See https://aka.ms/blazor-offline-considerations
|
||||||
|
|
||||||
|
self.importScripts('./service-worker-assets.js');
|
||||||
|
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
|
||||||
|
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
|
||||||
|
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
|
||||||
|
|
||||||
|
const cacheNamePrefix = 'offline-cache-';
|
||||||
|
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
|
||||||
|
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/ ];
|
||||||
|
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
|
||||||
|
|
||||||
|
async function onInstall(event) {
|
||||||
|
console.info('Service worker: Install');
|
||||||
|
|
||||||
|
// Fetch and cache all matching items from the assets manifest
|
||||||
|
const assetsRequests = self.assetsManifest.assets
|
||||||
|
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
|
||||||
|
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
|
||||||
|
.map(asset => new Request(asset.url, { integrity: asset.hash }));
|
||||||
|
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onActivate(event) {
|
||||||
|
console.info('Service worker: Activate');
|
||||||
|
|
||||||
|
// Delete unused caches
|
||||||
|
const cacheKeys = await caches.keys();
|
||||||
|
await Promise.all(cacheKeys
|
||||||
|
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
|
||||||
|
.map(key => caches.delete(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFetch(event) {
|
||||||
|
let cachedResponse = null;
|
||||||
|
if (event.request.method === 'GET') {
|
||||||
|
// For all navigation requests, try to serve index.html from cache
|
||||||
|
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
|
||||||
|
const shouldServeIndexHtml = event.request.mode === 'navigate';
|
||||||
|
|
||||||
|
const request = shouldServeIndexHtml ? 'index.html' : event.request;
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
cachedResponse = await cache.match(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedResponse || fetch(event.request);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Templates.Test.Helpers;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Templates.Test
|
||||||
|
{
|
||||||
|
public class BaselineTest
|
||||||
|
{
|
||||||
|
private static readonly Regex TemplateNameRegex = new Regex(
|
||||||
|
"new (?<template>[a-zA-Z]+)",
|
||||||
|
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
|
||||||
|
TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
private static readonly Regex AuthenticationOptionRegex = new Regex(
|
||||||
|
"-au (?<auth>[a-zA-Z]+)",
|
||||||
|
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
|
||||||
|
TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
private static readonly Regex LanguageRegex = new Regex(
|
||||||
|
"--language (?<language>\\w+)",
|
||||||
|
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
|
||||||
|
TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
public BaselineTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
ProjectFactory = projectFactory;
|
||||||
|
Output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Project Project { get; set; }
|
||||||
|
|
||||||
|
public static TheoryData<string, string[]> TemplateBaselines
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using (var stream = typeof(BaselineTest).Assembly.GetManifestResourceStream("ProjectTemplates.Tests.template-baselines.json"))
|
||||||
|
{
|
||||||
|
using (var jsonReader = new JsonTextReader(new StreamReader(stream)))
|
||||||
|
{
|
||||||
|
var baseline = JObject.Load(jsonReader);
|
||||||
|
var data = new TheoryData<string, string[]>();
|
||||||
|
foreach (var template in baseline)
|
||||||
|
{
|
||||||
|
foreach (var authOption in (JObject)template.Value)
|
||||||
|
{
|
||||||
|
data.Add(
|
||||||
|
(string)authOption.Value["Arguments"],
|
||||||
|
((JArray)authOption.Value["Files"]).Select(s => (string)s).ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectFactoryFixture ProjectFactory { get; }
|
||||||
|
public ITestOutputHelper Output { get; }
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TemplateBaselines))]
|
||||||
|
public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles)
|
||||||
|
{
|
||||||
|
Project = await ProjectFactory.GetOrCreateProject("baseline" + SanitizeArgs(arguments), Output);
|
||||||
|
var createResult = await Project.RunDotNetNewRawAsync(arguments);
|
||||||
|
Assert.True(createResult.ExitCode == 0, createResult.GetFormattedOutput());
|
||||||
|
|
||||||
|
foreach (var file in expectedFiles)
|
||||||
|
{
|
||||||
|
AssertFileExists(Project.TemplateOutputDir, file, shouldExist: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filesInFolder = Directory.EnumerateFiles(Project.TemplateOutputDir, "*", SearchOption.AllDirectories);
|
||||||
|
foreach (var file in filesInFolder)
|
||||||
|
{
|
||||||
|
var relativePath = file.Replace(Project.TemplateOutputDir, "").Replace("\\", "/").Trim('/');
|
||||||
|
if (relativePath.EndsWith(".csproj", StringComparison.Ordinal) ||
|
||||||
|
relativePath.EndsWith(".fsproj", StringComparison.Ordinal) ||
|
||||||
|
relativePath.EndsWith(".props", StringComparison.Ordinal) ||
|
||||||
|
relativePath.EndsWith(".sln", StringComparison.Ordinal) ||
|
||||||
|
relativePath.EndsWith(".targets", StringComparison.Ordinal) ||
|
||||||
|
relativePath.StartsWith("bin/", StringComparison.Ordinal) ||
|
||||||
|
relativePath.StartsWith("obj/", StringComparison.Ordinal) ||
|
||||||
|
relativePath.Contains("/bin/", StringComparison.Ordinal) ||
|
||||||
|
relativePath.Contains("/obj/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Assert.Contains(relativePath, expectedFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SanitizeArgs(string arguments)
|
||||||
|
{
|
||||||
|
var text = TemplateNameRegex.Match(arguments)
|
||||||
|
.Groups.TryGetValue("template", out var template) ? template.Value : "";
|
||||||
|
|
||||||
|
text += AuthenticationOptionRegex.Match(arguments)
|
||||||
|
.Groups.TryGetValue("auth", out var auth) ? auth.Value : "";
|
||||||
|
|
||||||
|
text += arguments.Contains("--uld") ? "uld" : "";
|
||||||
|
|
||||||
|
text += LanguageRegex.Match(arguments)
|
||||||
|
.Groups.TryGetValue("language", out var language) ? language.Value.Replace("#", "Sharp") : "";
|
||||||
|
|
||||||
|
if (arguments.Contains("--support-pages-and-views true"))
|
||||||
|
{
|
||||||
|
text += "supportpagesandviewstrue";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.Contains("-ho"))
|
||||||
|
{
|
||||||
|
text += "hosted";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.Contains("--pwa"))
|
||||||
|
{
|
||||||
|
text += "pwa";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertFileExists(string basePath, string path, bool shouldExist)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(basePath, path);
|
||||||
|
var doesExist = File.Exists(fullPath);
|
||||||
|
|
||||||
|
if (shouldExist)
|
||||||
|
{
|
||||||
|
Assert.True(doesExist, "Expected file to exist, but it doesn't: " + path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.False(doesExist, "Expected file not to exist, but it does: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -24,6 +25,11 @@ namespace Templates.Test
|
||||||
|
|
||||||
public ProjectFactoryFixture ProjectFactory { get; set; }
|
public ProjectFactoryFixture ProjectFactory { get; set; }
|
||||||
|
|
||||||
|
public override Task InitializeAsync()
|
||||||
|
{
|
||||||
|
return InitializeAsync(isolationContext: Guid.NewGuid().ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BlazorWasmStandaloneTemplate_Works()
|
public async Task BlazorWasmStandaloneTemplate_Works()
|
||||||
{
|
{
|
||||||
|
|
@ -90,6 +96,51 @@ namespace Templates.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BlazorWasmPwaTemplate_Works()
|
||||||
|
{
|
||||||
|
var project = await ProjectFactory.GetOrCreateProject("blazorpwa", Output);
|
||||||
|
project.TargetFramework = "netstandard2.1";
|
||||||
|
|
||||||
|
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--pwa" });
|
||||||
|
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
|
||||||
|
|
||||||
|
var publishResult = await project.RunDotNetPublishAsync();
|
||||||
|
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
|
||||||
|
|
||||||
|
var buildResult = await project.RunDotNetBuildAsync();
|
||||||
|
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
|
||||||
|
|
||||||
|
await BuildAndRunTest(project.ProjectName, project);
|
||||||
|
|
||||||
|
var publishDir = Path.Combine(project.TemplatePublishDir, project.ProjectName, "dist");
|
||||||
|
AspNetProcess.EnsureDevelopmentCertificates();
|
||||||
|
|
||||||
|
// When publishing the PWA template, we generate an assets manifest
|
||||||
|
// and move service-worker.published.js to overwrite service-worker.js
|
||||||
|
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
|
||||||
|
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
|
||||||
|
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
|
||||||
|
|
||||||
|
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
|
||||||
|
var listeningUri = "https://localhost:8080";
|
||||||
|
|
||||||
|
Output.WriteLine("Running dotnet serve on published output...");
|
||||||
|
using (var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), "serve -S"))
|
||||||
|
{
|
||||||
|
Output.WriteLine($"Opening browser at {listeningUri}...");
|
||||||
|
Browser.Navigate().GoToUrl(listeningUri);
|
||||||
|
TestBasicNavigation(project.ProjectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PWA template supports offline use. By now, the browser should have cached everything it needs,
|
||||||
|
// so we can continue working even without the server.
|
||||||
|
Browser.Navigate().GoToUrl("about:blank"); // Be sure we're really reloading
|
||||||
|
Output.WriteLine($"Opening browser without corresponding server at {listeningUri}...");
|
||||||
|
Browser.Navigate().GoToUrl(listeningUri);
|
||||||
|
TestBasicNavigation(project.ProjectName);
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task BuildAndRunTest(string appName, Project project)
|
protected async Task BuildAndRunTest(string appName, Project project)
|
||||||
{
|
{
|
||||||
using var aspNetProcess = project.StartBuiltProjectAsync();
|
using var aspNetProcess = project.StartBuiltProjectAsync();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
@ -68,6 +69,15 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
{
|
{
|
||||||
browser.Dispose();
|
browser.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var context in _browsers.Keys)
|
||||||
|
{
|
||||||
|
var userProfileDirectory = UserProfileDirectory(context);
|
||||||
|
if (!string.IsNullOrEmpty(userProfileDirectory) && Directory.Exists(userProfileDirectory))
|
||||||
|
{
|
||||||
|
Directory.Delete(userProfileDirectory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output, string isolationContext = "")
|
public Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output, string isolationContext = "")
|
||||||
|
|
@ -116,6 +126,13 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
output.WriteLine($"Set {nameof(ChromeOptions)}.{nameof(opts.BinaryLocation)} to {binaryLocation}");
|
output.WriteLine($"Set {nameof(ChromeOptions)}.{nameof(opts.BinaryLocation)} to {binaryLocation}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userProfileDirectory = UserProfileDirectory(context);
|
||||||
|
if (!string.IsNullOrEmpty(userProfileDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(userProfileDirectory);
|
||||||
|
opts.AddArgument($"--user-data-dir={userProfileDirectory}");
|
||||||
|
}
|
||||||
|
|
||||||
var instance = await SeleniumStandaloneServer.GetInstanceAsync(output);
|
var instance = await SeleniumStandaloneServer.GetInstanceAsync(output);
|
||||||
|
|
||||||
var attempt = 0;
|
var attempt = 0;
|
||||||
|
|
@ -155,6 +172,16 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
|
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string UserProfileDirectory(string context)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(context))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(Path.GetTempPath(), "BrowserFixtureUserProfiles", context);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(IWebDriver browser, ILogs log)> CreateSauceBrowserAsync(string context, ITestOutputHelper output)
|
private async Task<(IWebDriver browser, ILogs log)> CreateSauceBrowserAsync(string context, ITestOutputHelper output)
|
||||||
{
|
{
|
||||||
var sauce = E2ETestOptions.Instance.Sauce;
|
var sauce = E2ETestOptions.Instance.Sauce;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue