From ad7e98be04fe7134a2f047d834a0e73e987e5c6e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 13 Jul 2018 09:49:34 +0100 Subject: [PATCH] Make index.html static again (#1123) * Add build command for generating the new boot JSON file * Remove build command for generating index.html * Update build targets to generate blazor.boot.json * Change SPA fallback routing to look for index.html in regular wwwroot. Will need to update setup for published apps later. * Stop autorebuild when index.html changes, since we no longer 'build' that file * Update Boot.WebAssembly.ts logic to use boot JSON info on startup * Restore support for loading CSS/JS from Blazor library projects * Use new startup script tag in all samples and templates * Fix MonoSanity sample - it must now be an exe because we use that as the trigger to generate the boot json (not the presence of index.html any more) * Fix SPA fallback routing for published apps Because in a previous commit, I changed it to look for index.html inside "wwwroot" instead of "dist" (because we no longer build it). But "wwwroot" doesn't exist for published apps, because static file servers wouldn't understand it. * CR: Fix path normalization --- .../HostedInAspNet.Client/wwwroot/index.html | 4 +- samples/MonoSanity/wwwroot/index.html | 1 - .../MonoSanityClient/MonoSanityClient.csproj | 3 +- samples/MonoSanityClient/Program.cs | 5 +- samples/StandaloneApp/wwwroot/index.html | 4 +- .../src/Boot.WebAssembly.ts | 69 +++-- .../src/Platform/Mono/MonoPlatform.ts | 5 +- ...HtmlCommand.cs => WriteBootJsonCommand.cs} | 16 +- .../Cli/Program.cs | 6 +- .../Core/BootJsonWriter.cs | 94 +++++++ .../EmbeddedResourcesProcessor.cs | 4 +- .../Core/IndexHtmlWriter.cs | 254 ------------------ .../Microsoft.AspNetCore.Blazor.Build.csproj | 1 - .../targets/Blazor.MonoRuntime.props | 4 +- .../targets/Blazor.MonoRuntime.targets | 96 ++++--- .../AutoRebuild/AutoRebuildExtensions.cs | 5 +- .../BlazorAppBuilderExtensions.cs | 62 ++++- .../wwwroot/index.html | 4 +- .../wwwroot/index.html | 4 +- .../BootJsonWriterTest.cs | 62 +++++ .../IndexHtmlWriterTest.cs | 116 -------- ...rosoft.AspNetCore.Blazor.Build.Test.csproj | 1 - test/testapps/BasicTestApp/wwwroot/index.html | 6 +- 23 files changed, 332 insertions(+), 494 deletions(-) rename src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/{BuildIndexHtmlCommand.cs => WriteBootJsonCommand.cs} (84%) create mode 100644 src/Microsoft.AspNetCore.Blazor.Build/Core/BootJsonWriter.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Build.Test/BootJsonWriterTest.cs delete mode 100644 test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs diff --git a/samples/HostedInAspNet.Client/wwwroot/index.html b/samples/HostedInAspNet.Client/wwwroot/index.html index ed1a339a4f..a47b1ad84e 100644 --- a/samples/HostedInAspNet.Client/wwwroot/index.html +++ b/samples/HostedInAspNet.Client/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -7,6 +7,6 @@ Loading... - + diff --git a/samples/MonoSanity/wwwroot/index.html b/samples/MonoSanity/wwwroot/index.html index 2d0cb1ee94..6fefd738b6 100644 --- a/samples/MonoSanity/wwwroot/index.html +++ b/samples/MonoSanity/wwwroot/index.html @@ -65,7 +65,6 @@

Loading...

- + diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.WebAssembly.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.WebAssembly.ts index 364d896ed1..249b716267 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.WebAssembly.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.WebAssembly.ts @@ -15,26 +15,20 @@ async function boot() { renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress)); }; - // Read startup config from the `); + const scriptElem = document.createElement('script'); + scriptElem.src = monoRuntimeScriptUrl; + scriptElem.defer = true; + document.body.appendChild(scriptElem); } function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) { diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/WriteBootJsonCommand.cs similarity index 84% rename from src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs rename to src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/WriteBootJsonCommand.cs index 490c3c0e46..275f4e7575 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/WriteBootJsonCommand.cs @@ -1,20 +1,16 @@ -// 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. +using Microsoft.Extensions.CommandLineUtils; using System; using System.IO; -using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands { - internal class BuildIndexHtmlCommand + internal class WriteBootJsonCommand { public static void Command(CommandLineApplication command) { - var clientPage = command.Option("--html-page", - "Path to the HTML Page containing the Blazor bootstrap script tag.", - CommandOptionType.SingleValue); - var referencesFile = command.Option("--references", "The path to a file that lists the paths to given referenced dll files", CommandOptionType.SingleValue); @@ -36,8 +32,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands command.OnExecute(() => { - if (string.IsNullOrEmpty(mainAssemblyPath.Value) || - !clientPage.HasValue() || !outputPath.HasValue()) + if (string.IsNullOrEmpty(mainAssemblyPath.Value) || !outputPath.HasValue()) { command.ShowHelp(command.Name); return 1; @@ -53,8 +48,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands ? File.ReadAllLines(embeddedResourcesFile.Value()) : Array.Empty(); - IndexHtmlWriter.UpdateIndex( - clientPage.Value(), + BootJsonWriter.WriteFile( mainAssemblyPath.Value, referencesSources, embeddedResourcesSources, diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs index db72e2ba8f..1eac086fc7 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs @@ -1,4 +1,4 @@ -// 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. using Microsoft.AspNetCore.Blazor.Build.Cli.Commands; @@ -15,9 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Build Name = "Microsoft.AspNetCore.Blazor.Build" }; app.HelpOption("-?|-h|--help"); - - app.Command("build", BuildIndexHtmlCommand.Command); + app.Command("resolve-dependencies", ResolveRuntimeDependenciesCommand.Command); + app.Command("write-boot-json", WriteBootJsonCommand.Command); if (args.Length > 0) { diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/BootJsonWriter.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/BootJsonWriter.cs new file mode 100644 index 0000000000..50a09fa8b2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/BootJsonWriter.cs @@ -0,0 +1,94 @@ +// 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 Microsoft.JSInterop; +using Mono.Cecil; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.AspNetCore.Blazor.Build +{ + internal class BootJsonWriter + { + public static void WriteFile( + string assemblyPath, + string[] assemblyReferences, + string[] embeddedResourcesSources, + bool linkerEnabled, + string outputPath) + { + var embeddedContent = EmbeddedResourcesProcessor.ExtractEmbeddedResources( + embeddedResourcesSources, Path.GetDirectoryName(outputPath)); + var bootJsonText = GetBootJsonContent( + Path.GetFileName(assemblyPath), + GetAssemblyEntryPoint(assemblyPath), + assemblyReferences, + embeddedContent, + linkerEnabled); + var normalizedOutputPath = Path.GetFullPath(outputPath); + Console.WriteLine("Writing boot data to: " + normalizedOutputPath); + File.WriteAllText(normalizedOutputPath, bootJsonText); + } + + public static string GetBootJsonContent(string assemblyFileName, string entryPoint, string[] assemblyReferences, IEnumerable embeddedContent, bool linkerEnabled) + { + var data = new BootJsonData( + assemblyFileName, + entryPoint, + assemblyReferences, + embeddedContent, + linkerEnabled); + return Json.Serialize(data); + } + + private static string GetAssemblyEntryPoint(string assemblyPath) + { + using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath)) + { + var entryPoint = assemblyDefinition.EntryPoint; + if (entryPoint == null) + { + throw new ArgumentException($"The assembly at {assemblyPath} has no specified entry point."); + } + + return $"{entryPoint.DeclaringType.FullName}::{entryPoint.Name}"; + } + } + + /// + /// Defines the structure of a Blazor boot JSON file + /// + class BootJsonData + { + public string Main { get; } + public string EntryPoint { get; } + public IEnumerable AssemblyReferences { get; } + public IEnumerable CssReferences { get; } + public IEnumerable JsReferences { get; } + public bool LinkerEnabled { get; } + + public BootJsonData( + string entrypointAssemblyWithExtension, + string entryPoint, + IEnumerable assemblyReferences, + IEnumerable embeddedContent, + bool linkerEnabled) + { + Main = entrypointAssemblyWithExtension; + EntryPoint = entryPoint; + AssemblyReferences = assemblyReferences; + LinkerEnabled = linkerEnabled; + + CssReferences = embeddedContent + .Where(c => c.Kind == EmbeddedResourceKind.Css) + .Select(c => c.RelativePath); + + JsReferences = embeddedContent + .Where(c => c.Kind == EmbeddedResourceKind.JavaScript) + .Select(c => c.RelativePath); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs index 12e07e1d51..21a28597e1 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs @@ -1,4 +1,4 @@ -// 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. using Mono.Cecil; @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Blazor.Build } WriteResourceFile(embeddedResource, outputPath); - // The URLs we write into the index.html file need to use web-style directory separators + // The URLs we write into the boot json file need to use web-style directory separators extractedResourceInfo = new EmbeddedResourceInfo(kind, EnsureHasPathSeparators(name, '/')); return true; } diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs deleted file mode 100644 index 535484c9fe..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs +++ /dev/null @@ -1,254 +0,0 @@ -// 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 AngleSharp; -using AngleSharp.Extensions; -using AngleSharp.Html; -using AngleSharp.Parser.Html; -using Mono.Cecil; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Microsoft.AspNetCore.Blazor.Build -{ - internal class IndexHtmlWriter - { - public static void UpdateIndex( - string path, - string assemblyPath, - IEnumerable assemblyReferences, - IEnumerable embeddedResourcesSources, - bool linkerEnabled, - string outputPath) - { - var template = GetTemplate(path); - if (template == null) - { - return; - } - var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); - var entryPoint = GetAssemblyEntryPoint(assemblyPath); - var embeddedContent = EmbeddedResourcesProcessor.ExtractEmbeddedResources( - embeddedResourcesSources, Path.GetDirectoryName(outputPath)); - var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, embeddedContent, linkerEnabled); - var normalizedOutputPath = Normalize(outputPath); - Console.WriteLine("Writing index to: " + normalizedOutputPath); - File.WriteAllText(normalizedOutputPath, updatedContent); - } - - private static string Normalize(string outputPath) => - Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileName(outputPath).ToLowerInvariant()); - - private static string GetTemplate(string path) - { - var fileName = Path.GetFileName(path); - if (File.Exists(path)) - { - return File.ReadAllText(path); - } - - if (Directory.Exists(Path.GetDirectoryName(path))) - { - var files = Directory.EnumerateFiles(Path.GetDirectoryName(path)) - .OrderBy(f => f); - foreach (var file in files) - { - if (string.Equals(fileName, Path.GetFileName(file), StringComparison.OrdinalIgnoreCase)) - { - return File.ReadAllText(file); - } - } - } - - return null; - } - - private static string GetAssemblyEntryPoint(string assemblyPath) - { - using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath)) - { - var entryPoint = assemblyDefinition.EntryPoint; - if (entryPoint == null) - { - throw new ArgumentException($"The assembly at {assemblyPath} has no specified entry point."); - } - - return $"{entryPoint.DeclaringType.FullName}::{entryPoint.Name}"; - } - } - - /// - /// Injects the Blazor boot code and supporting config data at a user-designated - /// script tag identified with a type of blazor-boot. - /// - /// - /// - /// If a matching script tag is found, then it will be adjusted to inject - /// supporting configuration data, including a src attribute that - /// will load the Blazor client-side library. Any existing attribute - /// names that match the boot config data will be overwritten, but other - /// user-supplied attributes will be left intact. This allows, for example, - /// to designate asynchronous loading or deferred running of the script - /// reference. - /// - /// If no matching script tag is found, it is assumed that the user is - /// responsible for completing the Blazor boot process. - /// - /// - public static string GetIndexHtmlContents( - string htmlTemplate, - string assemblyName, - string assemblyEntryPoint, - IEnumerable assemblyReferences, - IEnumerable embeddedContent, - bool linkerEnabled) - { - var resultBuilder = new StringBuilder(); - - // Search for a tag of the form , and replace - // it with a fully-configured Blazor boot script tag - var textSource = new TextSource(htmlTemplate); - var currentRangeStartPos = 0; - var isInBlazorBootTag = false; - var resumeOnNextToken = false; - var result = string.Empty; - - foreach (var token in textSource.Tokenize(HtmlEntityService.Resolver)) - { - var tokenCharIndex = token.Position.Position - 1; - if (resumeOnNextToken) - { - resumeOnNextToken = false; - currentRangeStartPos = tokenCharIndex; - } - - switch (token.Type) - { - case HtmlTokenType.StartTag: - { - // Only do anything special if this is a Blazor boot tag - var tag = token.AsTag(); - if (IsBlazorBootTag(tag)) - { - // First, emit the original source text prior to this special tag, since - // we want that to be unchanged - resultBuilder.Append(htmlTemplate, currentRangeStartPos, tokenCharIndex - currentRangeStartPos); - - // Instead of emitting the source text for this special tag, emit a fully- - // configured Blazor boot script tag - AppendScriptTagWithBootConfig( - resultBuilder, - assemblyName, - assemblyEntryPoint, - assemblyReferences, - linkerEnabled, - tag.Attributes); - - // Emit tags to reference any specified JS/CSS files - AppendReferenceTags( - resultBuilder, - embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.Css).Select(c => c.RelativePath), - ""); - AppendReferenceTags( - resultBuilder, - embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.JavaScript).Select(c => c.RelativePath), - ""); - - // Set a flag so we know not to emit anything else until the special - // tag is closed - isInBlazorBootTag = true; - } - break; - } - - case HtmlTokenType.EndTag: - // If this is an end tag corresponding to the Blazor boot script tag, we - // can switch back into the mode of emitting the original source text - if (isInBlazorBootTag) - { - isInBlazorBootTag = false; - resumeOnNextToken = true; - } - break; - - case HtmlTokenType.EndOfFile: - // Finally, emit any remaining text from the original source file - var remainingLength = htmlTemplate.Length - currentRangeStartPos; - if (remainingLength > 0) - { - resultBuilder.Append(htmlTemplate, currentRangeStartPos, remainingLength); - } - result = resultBuilder.ToString(); - break; - } - } - - - return result; - } - - private static void AppendReferenceTags(StringBuilder resultBuilder, IEnumerable urls, string format) - { - foreach (var url in urls) - { - resultBuilder.AppendLine(); - resultBuilder.AppendFormat(format, url); - } - } - - private static bool IsBlazorBootTag(HtmlTagToken tag) - => string.Equals(tag.Name, "script", StringComparison.Ordinal) - && tag.Attributes.Any(pair => - string.Equals(pair.Key, "type", StringComparison.Ordinal) - && string.Equals(pair.Value, "blazor-boot", StringComparison.Ordinal)); - - private static void AppendScriptTagWithBootConfig( - StringBuilder resultBuilder, - string assemblyName, - string assemblyEntryPoint, - IEnumerable binFiles, - bool linkerEnabled, - List> attributes) - { - var assemblyNameWithExtension = $"{assemblyName}.dll"; - - var referencesAttribute = string.Join(",", binFiles.ToArray()); - - var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value); - attributesDict.Remove("type"); - attributesDict["src"] = "_framework/blazor.webassembly.js"; - attributesDict["main"] = assemblyNameWithExtension; - attributesDict["entrypoint"] = assemblyEntryPoint; - attributesDict["references"] = referencesAttribute; - - if (linkerEnabled) - { - attributesDict["linker-enabled"] = "true"; - } - else - { - attributesDict.Remove("linker-enabled"); - } - - resultBuilder.Append(""); - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj index e0abfc9c76..29596e9aa4 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj @@ -46,7 +46,6 @@ - diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props index 610418d64a..07dcf437b4 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props +++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props @@ -21,8 +21,8 @@ $(BaseBlazorRuntimeOutputPath) blazor/ wwwroot/ - Index.html - $(BlazorIndexHtmlName.ToLowerInvariant()) + blazor.boot.json + $(BaseBlazorRuntimeOutputPath)$(BlazorBootJsonName) diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets index 7fbb72df41..f93fc4a30b 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets +++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets @@ -5,7 +5,8 @@ DependsOnTargets="PrepareBlazorOutputs" Inputs="@(BlazorItemOutput)" Outputs="@(BlazorItemOutput->'%(TargetOutputPath)')" - AfterTargets="CopyFilesToOutputDirectory"> + AfterTargets="CopyFilesToOutputDirectory" + Condition="'$(OutputType.ToLowerInvariant())'=='exe'"> @@ -102,7 +103,7 @@ _PrepareBlazorOutputConfiguration; _DefineBlazorCommonInputs; _BlazorResolveOutputBinaries; - _GenerateBlazorIndexHtml; + _GenerateBlazorBootJson; @@ -124,8 +125,7 @@ /_framework/asmjs <- This will contain the asmjs runtime copied from the nuget package. /_framework/wasm <- This will contain the wsm runtime copied from the nuget package. /_framework/blazor.js <- This is the blazor.js file copied from the nuget package. - /index.html <- This is the optional index.html file generated from wwwroot/Index.html in case it's present. It - will be canonicalized to index.html + /_framework/blazor.boot.json <- This is the boot json file This task also defines some intermediate paths that we will use: /obj/<>/<>/blazor/blazor/linker <- This will be used to create the output from the linker. @@ -145,8 +145,8 @@ /obj/<>/<>/blazor/resolvedassemblies/ <- This will be used to store the resolved assemblies before copying them to the output when linking is not enabled. /obj/<>/<>/blazor/resolved.assemblies.txt <- This keeps track of all the resolved assemblies. - /obj/<>/<>/blazor/index.html <- The generated index.html with the updated blazor script tag. - /obj/<>/<>/blazor/inputs.index.cache <- The marker file that track whether index.html needs to + /obj/<>/<>/blazor/blazor.boot.json <- The generated boot json file + /obj/<>/<>/blazor/inputs.bootjson.cache <- The marker file that track whether boot json needs to be regenerated. --> @@ -156,11 +156,6 @@ <_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml - - $(ProjectDir)$(BlazorWebRootName) - $(BlazorWebRootPath)$(BlazorIndexHtmlName) - - $(ProjectDir)$(OutputPath)$(BaseBlazorRuntimeAsmjsOutputPath)%(FileName)%(Extension) @@ -225,22 +220,22 @@ $(BlazorIntermediateOutputPath)resolved.assemblies.txt - + - $(BlazorIntermediateOutputPath) + $(BlazorIntermediateOutputPath) - - $(BlazorIndexHtmlOutputDir)$(BlazorOutputIndexHtmlName) + + $(BlazorBootJsonIntermediateOutputDir)$(BlazorBootJsonName) - - $(BlazorIntermediateOutputPath)inputs.index.cache + + $(BlazorIntermediateOutputPath)inputs.bootjson.cache $(BlazorIntermediateOutputPath)resolve-dependencies.txt - - $(BlazorIntermediateOutputPath)indexhtml-references.txt + + $(BlazorIntermediateOutputPath)bootjson-references.txt $(BlazorIntermediateOutputPath)embedded.resources.txt @@ -586,36 +581,35 @@ Final part of the build pipeline: * Collect the blazor application assemblies to be copied to the output and create a marker file. - * Call our CLI tool to generate the index html if the list of assemblies has changed. + * Call our CLI tool to generate the boot json if the list of assemblies has changed. --> - + - - - - - - + + + + + - + + Name="_GenerateBlazorBootJson" + DependsOnTargets="_ResolveBlazorBootJsonInputs" + Inputs="$(BlazorBuildBootJsonInputsCache);@(_BlazorDependencyInput)" + Outputs="$(BlazorBootJsonIntermediateOutputPath)"> <_UnlinkedAppReferencesPaths Include="@(_BlazorDependencyInput)" /> <_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" /> @@ -623,12 +617,12 @@ <_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled - <_ReferencesArg Condition="'@(_AppReferences)' != ''">--references "$(BlazorIndexHtmlReferencesFilePath)" + <_ReferencesArg Condition="'@(_AppReferences)' != ''">--references "$(BlazorBootJsonReferencesFilePath)" <_EmbeddedResourcesArg Condition="'@(_UnlinkedAppReferencesPaths)' != ''">--embedded-resources "$(BlazorEmbeddedResourcesConfigFilePath)" @@ -638,20 +632,20 @@ Lines="@(_UnlinkedAppReferencesPaths)" Overwrite="true" /> - + - - <_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" /> - <_BlazorIndexEmbeddedContentFile Include="$(BlazorIndexHtmlOutputDir)_content\**\*.*" /> - - $(ProjectDir)$(OutputPath)dist/%(FileName)%(Extension) - EntryPoint + + <_BlazorBootJson Include="$(BlazorBootJsonIntermediateOutputPath)" /> + <_BlazorBootJsonEmbeddedContentFile Include="$(BlazorBootJsonIntermediateOutputDir)_content\**\*.*" /> + + $(ProjectDir)$(OutputPath)$(BlazorBootJsonOutputPath) + BootJson - + $(ProjectDir)$(OutputPath)dist/_content/%(RecursiveDir)%(FileName)%(Extension) - - + + diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs index e5ad58f08e..89b8828bd0 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs @@ -16,10 +16,9 @@ namespace Microsoft.AspNetCore.Builder internal static class AutoRebuildExtensions { // Note that we don't need to watch typical static-file extensions (.css, .js, etc.) - // because anything in wwwroot is just served directly from disk on each reload. But - // as a special case, we do watch index.html because it needs compilation. + // because anything in wwwroot is just served directly from disk on each reload. // TODO: Make the set of extensions and exclusions configurable in csproj - private static string[] _includedSuffixes = new[] { ".cs", ".cshtml", "index.html" }; + private static string[] _includedSuffixes = new[] { ".cs", ".cshtml" }; private static string[] _excludedDirectories = new[] { "obj", "bin" }; // To ensure the FileSystemWatchers aren't collected, reference them diff --git a/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs index 4a9bcf8f31..b8f451573f 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Net.Http.Headers; using System.Net.Mime; using System; +using System.IO; namespace Microsoft.AspNetCore.Builder { @@ -48,12 +49,6 @@ namespace Microsoft.AspNetCore.Builder // hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either. var env = (IHostingEnvironment)app.ApplicationServices.GetService(typeof(IHostingEnvironment)); var config = BlazorConfig.Read(options.ClientAssemblyPath); - var distDirStaticFiles = new StaticFileOptions - { - FileProvider = new PhysicalFileProvider(config.DistPath), - ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging), - OnPrepareResponse = SetCacheHeaders - }; if (env.IsDevelopment() && config.EnableAutoRebuilding) { @@ -68,15 +63,21 @@ namespace Microsoft.AspNetCore.Builder } // First, match the request against files in the client app dist directory - app.UseStaticFiles(distDirStaticFiles); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(config.DistPath), + ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging), + OnPrepareResponse = SetCacheHeaders + }); - // Next, match the request against static files in wwwroot + // * Before publishing, we serve the wwwroot files directly from source + // (and don't require them to be copied into dist). + // In this case, WebRootPath will be nonempty if that directory exists. + // * After publishing, the wwwroot files are already copied to 'dist' and + // will be served by the above middleware, so we do nothing here. + // In this case, WebRootPath will be empty (the publish process sets this). if (!string.IsNullOrEmpty(config.WebRootPath)) { - // In development, we serve the wwwroot files directly from source - // (and don't require them to be copied into dist). - // TODO: When publishing is implemented, have config.WebRootPath set - // to null so that it only serves files that were copied to dist app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(config.WebRootPath), @@ -94,13 +95,48 @@ namespace Microsoft.AspNetCore.Builder // excluding /_framework/*) app.MapWhen(IsNotFrameworkDir, childAppBuilder => { + var indexHtmlPath = FindIndexHtmlFile(config); + var indexHtmlStaticFileOptions = string.IsNullOrEmpty(indexHtmlPath) + ? null : new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(Path.GetDirectoryName(indexHtmlPath)), + OnPrepareResponse = SetCacheHeaders + }; + childAppBuilder.UseSpa(spa => { - spa.Options.DefaultPageStaticFileOptions = distDirStaticFiles; + spa.Options.DefaultPageStaticFileOptions = indexHtmlStaticFileOptions; }); }); } + private static string FindIndexHtmlFile(BlazorConfig config) + { + // Before publishing, the client project may have a wwwroot directory. + // If so, and if it contains index.html, use that. + if (!string.IsNullOrEmpty(config.WebRootPath)) + { + var wwwrootIndexHtmlPath = Path.Combine(config.WebRootPath, "index.html"); + if (File.Exists(wwwrootIndexHtmlPath)) + { + return wwwrootIndexHtmlPath; + } + } + + // After publishing, the client project won't have a wwwroot directory. + // The contents from that dir will have been copied to "dist" during publish. + // So if "dist/index.html" now exists, use that. + var distIndexHtmlPath = Path.Combine(config.DistPath, "index.html"); + if (File.Exists(distIndexHtmlPath)) + { + return distIndexHtmlPath; + } + + // Since there's no index.html, we'll use the default DefaultPageStaticFileOptions, + // hence we'll look for index.html in the host server app's wwwroot. + return null; + } + private static void SetCacheHeaders(StaticFileResponseContext ctx) { // By setting "Cache-Control: no-cache", we're allowing the browser to store diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/wwwroot/index.html b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/wwwroot/index.html index 2f52d8478f..f7e0e86795 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/wwwroot/index.html +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -11,6 +11,6 @@ Loading... - + diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/wwwroot/index.html b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/wwwroot/index.html index a4f5dc8d03..f0f4c327c3 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/wwwroot/index.html +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -11,6 +11,6 @@ Loading... - + diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/BootJsonWriterTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/BootJsonWriterTest.cs new file mode 100644 index 0000000000..3632a082b7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/BootJsonWriterTest.cs @@ -0,0 +1,62 @@ +// 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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Build.Test +{ + public class BootJsonWriterTest + { + [Fact] + public void ProducesJsonReferencingAssemblyAndDependencies() + { + // Arrange/Act + var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", }; + var content = BootJsonWriter.GetBootJsonContent( + "MyApp.Entrypoint.dll", + "MyNamespace.MyType::MyMethod", + assemblyReferences, + Enumerable.Empty(), + linkerEnabled: true); + + // Assert + var parsedContent = JsonConvert.DeserializeObject(content); + Assert.Equal("MyApp.Entrypoint.dll", parsedContent["main"].Value()); + Assert.Equal("MyNamespace.MyType::MyMethod", parsedContent["entryPoint"].Value()); + Assert.Equal(assemblyReferences, parsedContent["assemblyReferences"].Values()); + } + + [Fact] + public void IncludesReferencesToEmbeddedContent() + { + // Arrange/Act + var embeddedContent = new[] + { + new EmbeddedResourceInfo(EmbeddedResourceKind.Static, "my/static/file"), + new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/first.css"), + new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/first.js"), + new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/second.css"), + new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/second.js"), + }; + var content = BootJsonWriter.GetBootJsonContent( + "MyApp.Entrypoint", + "MyNamespace.MyType::MyMethod", + assemblyReferences: new[] { "Something.dll" }, + embeddedContent: embeddedContent, + linkerEnabled: true); + + // Assert + var parsedContent = JsonConvert.DeserializeObject(content); + Assert.Equal( + new[] { "css/first.css", "css/second.css" }, + parsedContent["cssReferences"].Values()); + Assert.Equal( + new[] { "javascript/first.js", "javascript/second.js" }, + parsedContent["jsReferences"].Values()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs deleted file mode 100644 index d3cd45c9d8..0000000000 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs +++ /dev/null @@ -1,116 +0,0 @@ -// 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 AngleSharp.Parser.Html; -using System; -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Blazor.Build.Test -{ - public class IndexHtmlWriterTest - { - [Fact] - public void InjectsScriptTagReferencingAssemblyAndDependencies() - { - // Arrange - var htmlTemplatePrefix = @" - - -

Hello

- Some text - "; - var htmlTemplateSuffix = @" - - "; - var htmlTemplate = - $@"{htmlTemplatePrefix} - - {htmlTemplateSuffix}"; - var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", }; - var instance = IndexHtmlWriter.GetIndexHtmlContents( - htmlTemplate, - "MyApp.Entrypoint", - "MyNamespace.MyType::MyMethod", - assemblyReferences, - Enumerable.Empty(), - linkerEnabled: true); - - // Act & Assert: Start and end is not modified (including formatting) - Assert.StartsWith(htmlTemplatePrefix, instance); - Assert.EndsWith(htmlTemplateSuffix, instance); - - // Assert: Boot tag is correct - var scriptTagText = instance.Substring(htmlTemplatePrefix.Length, instance.Length - htmlTemplatePrefix.Length - htmlTemplateSuffix.Length); - var parsedHtml = new HtmlParser().Parse("" + scriptTagText + ""); - var scriptElems = parsedHtml.Body.QuerySelectorAll("script"); - var linkElems = parsedHtml.Body.QuerySelectorAll("link"); - var scriptElem = scriptElems[0]; - Assert.False(scriptElem.HasChildNodes); - Assert.Equal("_framework/blazor.webassembly.js", scriptElem.GetAttribute("src")); - Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main")); - Assert.Equal("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint")); - Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references")); - Assert.False(scriptElem.HasAttribute("type")); - Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value); - Assert.Equal("value", scriptElem.Attributes["custom2"].Value); - Assert.Equal("true", scriptElem.Attributes["linker-enabled"].Value); - } - - [Fact] - public void SuppliesHtmlTemplateUnchangedIfNoBootScriptPresent() - { - // Arrange - var htmlTemplate = "

Hello

Some text"; - var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll" }; - var jsReferences = new string[] { "some/file.js", "another.js" }; - var cssReferences = new string[] { "my/styles.css" }; - - var content = IndexHtmlWriter.GetIndexHtmlContents( - htmlTemplate, "MyApp.Entrypoint", "MyNamespace.MyType::MyMethod", assemblyReferences, Enumerable.Empty(), linkerEnabled: true); - - // Assert - Assert.Equal(htmlTemplate, content); - } - - [Fact] - public void InjectsAdditionalTagsForEmbeddedContent() - { - // Arrange - var htmlTemplate = "Start End"; - var embeddedContent = new[] - { - new EmbeddedResourceInfo(EmbeddedResourceKind.Static, "my/static/file"), - new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/first.css"), - new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/first.js"), - new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/second.css"), - new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/second.js"), - }; - - // Act - var resultHtml = IndexHtmlWriter.GetIndexHtmlContents( - htmlTemplate, - "MyApp.Entrypoint", - "MyNamespace.MyType::MyMethod", - assemblyReferences: new[] { "Something.dll" }, - embeddedContent: embeddedContent, - linkerEnabled: true); - - // Assert - var parsedHtml = new HtmlParser().Parse(resultHtml); - var blazorBootScript = parsedHtml.GetElementById("testboot"); - Assert.NotNull(blazorBootScript); - Assert.Equal( - "Start " - + blazorBootScript.OuterHtml - // First we insert the CSS file tags in order - + Environment.NewLine + "" - + Environment.NewLine + "" - // Then the JS file tags in order, each with 'defer' - + Environment.NewLine + "" - + Environment.NewLine + "" - + " End", - resultHtml); - } - } -} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/Microsoft.AspNetCore.Blazor.Build.Test.csproj b/test/Microsoft.AspNetCore.Blazor.Build.Test/Microsoft.AspNetCore.Blazor.Build.Test.csproj index 9c34250818..8a1f91a649 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/Microsoft.AspNetCore.Blazor.Build.Test.csproj +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/Microsoft.AspNetCore.Blazor.Build.Test.csproj @@ -19,7 +19,6 @@ - diff --git a/test/testapps/BasicTestApp/wwwroot/index.html b/test/testapps/BasicTestApp/wwwroot/index.html index 576ba709b1..07d610309a 100644 --- a/test/testapps/BasicTestApp/wwwroot/index.html +++ b/test/testapps/BasicTestApp/wwwroot/index.html @@ -47,9 +47,11 @@ Loading... - + + - + +