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... - + + - + +