Skip to content

Commit

Permalink
Reference static content from referenced assemblies. Implements #340
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS committed Apr 6, 2018
1 parent 2e41f05 commit c8575af
Show file tree
Hide file tree
Showing 20 changed files with 393 additions and 67 deletions.
7 changes: 7 additions & 0 deletions Blazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Performance", "benchmarks\Microsoft.AspNetCore.Blazor.Performance\Microsoft.AspNetCore.Blazor.Performance.csproj", "{50F6820F-D058-4E68-9E15-801F893F514E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorContent.CSharp", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorContent.CSharp\BlazorContent.CSharp.csproj", "{3A457B14-D91B-4FFF-A81A-8F350BDB911F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -324,6 +326,10 @@ Global
{50F6820F-D058-4E68-9E15-801F893F514E}.Release|Any CPU.Build.0 = Release|Any CPU
{50F6820F-D058-4E68-9E15-801F893F514E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{50F6820F-D058-4E68-9E15-801F893F514E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -365,6 +371,7 @@ Global
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
{50F6820F-D058-4E68-9E15-801F893F514E} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,11 @@ public static void Command(CommandLineApplication command)
CommandOptionType.SingleValue);

var references = command.Option("--reference",
"The path from the _bin folder to a given referenced dll file (Typically just the dll name)",
"The path from the _bin folder to a given referenced dll file (typically just the dll name)",
CommandOptionType.MultipleValue);

var jsReferences = command.Option("--js",
"Adds a <script> tag with the specified 'src' value",
CommandOptionType.MultipleValue);

var cssReferences = command.Option("--css",
"Adds a <link rel=stylesheet> tag with the specified 'href' value",
var embeddedResourcesSources = command.Option("--embedded-resources-source",
"The path to an assembly that may contain embedded resources (typically a referenced assembly in its pre-linked state)",
CommandOptionType.MultipleValue);

var outputPath = command.Option("--output",
Expand Down Expand Up @@ -52,8 +48,7 @@ public static void Command(CommandLineApplication command)
clientPage.Value(),
mainAssemblyPath.Value,
references.Values.ToArray(),
jsReferences.Values.ToArray(),
cssReferences.Values.ToArray(),
embeddedResourcesSources.Values.ToArray(),
linkerEnabledFlag.HasValue(),
outputPath.Value());
return 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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.

namespace Microsoft.AspNetCore.Blazor.Build
{
internal class EmbeddedResourceInfo
{
public EmbeddedResourceKind Kind { get; }
public string RelativePath { get; }

public EmbeddedResourceInfo(EmbeddedResourceKind kind, string relativePath)
{
Kind = kind;
RelativePath = relativePath;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.

namespace Microsoft.AspNetCore.Blazor.Build
{
internal enum EmbeddedResourceKind
{
JavaScript,
Css,
Static
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Microsoft.AspNetCore.Blazor.Build
{
internal class EmbeddedResourcesProcessor
{
const string ContentSubdirName = "_content";

private readonly static Dictionary<string, EmbeddedResourceKind> _knownResourceKindsByNamePrefix = new Dictionary<string, EmbeddedResourceKind>
{
{ "blazor:js:", EmbeddedResourceKind.JavaScript },
{ "blazor:css:", EmbeddedResourceKind.Css },
{ "blazor:file:", EmbeddedResourceKind.Static },
};

/// <summary>
/// Finds Blazor-specific embedded resources in the specified assemblies, writes them
/// to disk, and returns a description of those resources in dependency order.
/// </summary>
/// <param name="referencedAssemblyPaths">The paths to assemblies that may contain embedded resources.</param>
/// <param name="outputDir">The path to the directory where output is being written.</param>
/// <returns>A description of the embedded resources that were written to disk.</returns>
public static IReadOnlyList<EmbeddedResourceInfo> ExtractEmbeddedResources(
IEnumerable<string> referencedAssemblyPaths, string outputDir)
{
// Clean away any earlier state
var contentDir = Path.Combine(outputDir, ContentSubdirName);
if (Directory.Exists(contentDir))
{
Directory.Delete(contentDir, recursive: true);
}

// First, get an ordered list of AssemblyDefinition instances
var referencedAssemblyDefinitions = referencedAssemblyPaths
.Where(path => !Path.GetFileName(path).StartsWith("System.", StringComparison.Ordinal)) // Skip System.* because they are never going to contain embedded resources that we want
.Select(path => AssemblyDefinition.ReadAssembly(path))
.ToList();
referencedAssemblyDefinitions.Sort(OrderWithReferenceSubjectFirst);

// Now process them in turn
return referencedAssemblyDefinitions
.SelectMany(def => ExtractEmbeddedResourcesFromSingleAssembly(def, outputDir))
.ToList()
.AsReadOnly();
}

private static IEnumerable<EmbeddedResourceInfo> ExtractEmbeddedResourcesFromSingleAssembly(
AssemblyDefinition assemblyDefinition, string outputDirPath)
{
var assemblyName = assemblyDefinition.Name.Name;
foreach (var res in assemblyDefinition.MainModule.Resources)
{
if (TryExtractEmbeddedResource(assemblyName, res, outputDirPath, out var extractedResourceInfo))
{
yield return extractedResourceInfo;
}
}
}

private static bool TryExtractEmbeddedResource(string assemblyName, Resource resource, string outputDirPath, out EmbeddedResourceInfo extractedResourceInfo)
{
if (resource is EmbeddedResource embeddedResource)
{
if (TryInterpretLogicalName(resource.Name, out var kind, out var name))
{
// Prefix the output path with the assembly name to ensure no clashes
// Also be invariant to the OS on which the package was built
name = Path.Combine(ContentSubdirName, assemblyName, EnsureHasPathSeparators(name, Path.DirectorySeparatorChar));

// Write the file content to disk, ensuring we don't try to write outside the output root
var outputPath = Path.GetFullPath(Path.Combine(outputDirPath, name));
if (!outputPath.StartsWith(outputDirPath))
{
throw new InvalidOperationException($"Cannot write embedded resource from assembly '{assemblyName}' to '{outputPath}' because it is outside the expected directory {outputDirPath}");
}
WriteResourceFile(embeddedResource, outputPath);

// The URLs we write into the index.html file need to use web-style directory separators
extractedResourceInfo = new EmbeddedResourceInfo(kind, EnsureHasPathSeparators(name, '/'));
return true;
}
}

extractedResourceInfo = null;
return false;
}

private static void WriteResourceFile(EmbeddedResource resource, string outputPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
using (var outputStream = File.OpenWrite(outputPath))
{
resource.GetResourceStream().CopyTo(outputStream);
}
}

private static string EnsureHasPathSeparators(string name, char desiredSeparatorChar) => name
.Replace('\\', desiredSeparatorChar)
.Replace('/', desiredSeparatorChar);

private static bool TryInterpretLogicalName(string logicalName, out EmbeddedResourceKind kind, out string resolvedName)
{
foreach (var kvp in _knownResourceKindsByNamePrefix)
{
if (logicalName.StartsWith(kvp.Key, StringComparison.Ordinal))
{
kind = kvp.Value;
resolvedName = logicalName.Substring(kvp.Key.Length);
return true;
}
}

kind = default;
resolvedName = default;
return false;
}

// For each assembly B that references A, we want the resources from A to be loaded before
// the references for B (because B's resources might depend on A's resources)
private static int OrderWithReferenceSubjectFirst(AssemblyDefinition a, AssemblyDefinition b)
=> AssemblyHasReference(a, b) ? 1
: AssemblyHasReference(b, a) ? -1
: 0;

private static bool AssemblyHasReference(AssemblyDefinition from, AssemblyDefinition to)
=> from.MainModule.AssemblyReferences
.Select(reference => reference.Name)
.Contains(to.Name.Name, StringComparer.Ordinal);
}
}
19 changes: 10 additions & 9 deletions src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public static void UpdateIndex(
string path,
string assemblyPath,
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences,
IEnumerable<string> embeddedResourcesSources,
bool linkerEnabled,
string outputPath)
{
Expand All @@ -31,7 +30,9 @@ public static void UpdateIndex(
}
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
var entryPoint = GetAssemblyEntryPoint(assemblyPath);
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, jsReferences, cssReferences, linkerEnabled);
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);
Expand Down Expand Up @@ -101,8 +102,7 @@ public static string GetIndexHtmlContents(
string assemblyName,
string assemblyEntryPoint,
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences,
IEnumerable<EmbeddedResourceInfo> embeddedContent,
bool linkerEnabled)
{
var resultBuilder = new StringBuilder();
Expand All @@ -118,10 +118,11 @@ public static string GetIndexHtmlContents(
while (true)
{
var token = tokenizer.Get();
var tokenCharIndex = token.Position.Position - 1;
if (resumeOnNextToken)
{
resumeOnNextToken = false;
currentRangeStartPos = token.Position.Position;
currentRangeStartPos = tokenCharIndex;
}

switch (token.Type)
Expand All @@ -134,7 +135,7 @@ public static string GetIndexHtmlContents(
{
// First, emit the original source text prior to this special tag, since
// we want that to be unchanged
resultBuilder.Append(htmlTemplate, currentRangeStartPos, token.Position.Position - currentRangeStartPos - 1);
resultBuilder.Append(htmlTemplate, currentRangeStartPos, tokenCharIndex - currentRangeStartPos);

// Instead of emitting the source text for this special tag, emit a fully-
// configured Blazor boot script tag
Expand All @@ -149,11 +150,11 @@ public static string GetIndexHtmlContents(
// Emit tags to reference any specified JS/CSS files
AppendReferenceTags(
resultBuilder,
cssReferences,
embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.Css).Select(c => c.RelativePath),
"<link rel=\"stylesheet\" href=\"{0}\" />");
AppendReferenceTags(
resultBuilder,
jsReferences,
embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.JavaScript).Select(c => c.RelativePath),
"<script src=\"{0}\" defer></script>");

// Set a flag so we know not to emit anything else until the special
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,11 @@

<!-- Index.html related paths and markers -->

<!-- /obj/<<configuration>>/<<targetframework>>/blazor/ -->
<BlazorIndexHtmlOutputDir>$(BlazorIntermediateOutputPath)</BlazorIndexHtmlOutputDir>

<!-- /obj/<<configuration>>/<<targetframework>>/blazor/index.html -->
<BlazorIndexHtmlOutputPath>$(BlazorIntermediateOutputPath)$(BlazorOutputIndexHtmlName)</BlazorIndexHtmlOutputPath>
<BlazorIndexHtmlOutputPath>$(BlazorIndexHtmlOutputDir)$(BlazorOutputIndexHtmlName)</BlazorIndexHtmlOutputPath>

<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.index.cache -->
<BlazorBuildIndexInputsCache>$(BlazorIntermediateOutputPath)inputs.index.cache</BlazorBuildIndexInputsCache>
Expand Down Expand Up @@ -568,8 +571,6 @@
<ItemGroup>
<BlazorIndexHtmlInput Include="$(BlazorIndexHtml)" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorPackageJsRef->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorPackageCssRef->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(_BlazorLinkingOption)" />
</ItemGroup>

Expand All @@ -588,26 +589,30 @@
<Target
Name="_GenerateBlazorIndexHtml"
DependsOnTargets="_ResolveBlazorIndexHtmlInputs"
Inputs="$(BlazorBuildIndexInputsCache);$(BlazorIndexHtml)"
Inputs="$(BlazorBuildIndexInputsCache);$(BlazorIndexHtml);@(_BlazorDependencyInput)"
Outputs="$(BlazorIndexHtmlOutputPath)">
<ItemGroup>
<_UnlinkedAppReferencesPaths Include="@(_BlazorDependencyInput)" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" />
<_JsReferences Include="@(BlazorPackageJsRef->'_content/%(SourcePackage)/%(RecursiveDir)%(FileName)%(Extension)')" />
<_CssReferences Include="@(BlazorPackageCssRef->'_content/%(SourcePackage)/%(RecursiveDir)%(FileName)%(Extension)')" />
</ItemGroup>
<PropertyGroup>
<_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled</_LinkerEnabledFlag>
</PropertyGroup>

<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference &quot;%(Identity)&quot;', ' ') @(_JsReferences->'--js &quot;%(Identity)&quot;', ' ') @(_CssReferences->'--css &quot;%(Identity)&quot;', ' ') $(_LinkerEnabledFlag) --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference &quot;%(Identity)&quot;', ' ') @(_UnlinkedAppReferencesPaths->'--embedded-resources-source &quot;%(Identity)&quot;', ' ') $(_LinkerEnabledFlag) --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />

<ItemGroup Condition="Exists('$(BlazorIndexHtmlOutputPath)')">
<_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" />
<_BlazorIndexEmbeddedContentFile Include="$(BlazorIndexHtmlOutputDir)_content\**\*.*" />
<BlazorItemOutput Include="@(_BlazorIndex)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)dist/%(FileName)%(Extension)</TargetOutputPath>
<Type>EntryPoint</Type>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_BlazorIndexEmbeddedContentFile)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)dist/_content/%(RecursiveDir)%(FileName)%(Extension)</TargetOutputPath>
</BlazorItemOutput>
<FileWrites Include="$(BlazorIndexHtmlOutputPath)" />
<FileWrites Include="@(_BlazorIndexEmbeddedContentFile)" />
</ItemGroup>

</Target>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/dotnetcli.host",
"symbolInfo": {
"skipRestore": {
"longName": "no-restore",
"shortName": ""
},
"Framework": {
"longName": "framework"
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c8575af

Please sign in to comment.