📜 ⬆️ ⬇️

Bundle Transformer: Summer Updates

Bundle Transformer logo on an agricultural field

Since September last year, when the MSIE JavaScript Engine for .NET library was replaced by the JavaScript Engine Switcher library and the BundleTransformer.CleanCss module was created, there were almost no revolutionary changes in the Bundle Transformer. The changes were mainly evolutionary: support for new versions of minimizers and translators was added (the most routine and difficult part of the project), minor bugs were fixed, and work went on to increase productivity.

But this summer everything changed: from the end of May to July, a huge number of recommendations for improving the project were received from users of Bundle Transformer. Most of them were implemented in version 1.9.0 and subsequent summer updates. In this article we consider the most significant of them:

Classes StyleTransformer and ScriptTransformer


Initially, not entirely successful names were chosen for the CssTransformer and JsTransformer classes, because the choice of names was made by analogy with the CssMinify and JsMinify from System.Web.Optimization . When StyleBundle and ScriptBundle classes appeared in System.Web.Optimization, it became clear that these names are no good.
')
For a long time I postponed the renaming of these classes for later, but when working on version 1.9.0 I decided to rename them all the same. Now they are called StyleTransformer and ScriptTransformer . The old CssTransformer and JsTransformer classes are still available in the kernel (implemented as wrappers over new classes), but they are considered obsolete (marked with the Obsolete attribute) and will be removed in version 2.0.0.

Postprocessors


In late May, I received a pull request "Support for Autoprefixer" from Vegard Larsen (an employee of the Norwegian company Digital Creations ). When viewing the code, it immediately became clear that the current architecture of the Bundle Transformer is not suitable for implementing such modules. Vegard implemented the functionality of this module as a style translator, which should have been launched after all other translators (LESS, Sass, etc.). This whole implementation looked like a hack, so I decided to reject this pull request. As a result, Vegard published an unofficial version of the module - BundleTransformer.Autoprefixer.Unofficial , in NuGet, and I began work on a new architecture of the Bundle Transformer.

A new type of modules was required, which should have been launched after translators and before minimizers, and the number and order of calling such modules should be determined by the developer. As a name for a new type of module, I decided to use the term “postprocessors”, which was invented by Andrei Sitnik (if you don’t know who Andrei Sitnik is or what postprocessors are, I recommend you to listen to the 6th edition of the Frontflip podcast ).

A postprocessor in a Bundle Transformer can be any class that implements the IPostProcessor interface or inherits the PostProcessorBase base class from the BundleTransformer.Core.PostProcessors namespace. Like other types of modules (adapters), postprocessors must be registered in the Web.config file. Consider the registration process on the example of CSS postprocessors:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> <css defaultPostProcessors="UrlRewritingCssPostProcessor,AutoprefixCssPostProcessor" …> <postProcessors> <add name="UrlRewritingCssPostProcessor" type="BundleTransformer.Core.PostProcessors.UrlRewritingCssPostProcessor, BundleTransformer.Core" useInDebugMode="false" /> <add name="AutoprefixCssPostProcessor" type="BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor, BundleTransformer.Autoprefixer" useInDebugMode="true" /> </postProcessors> </css> … </core> … </bundleTransformer> … </configuration> 

Two postprocessors are registered in the /configuration/bundleTransformer/core/css/postProcessors :
  1. UrlRewritingCssPostProcessor. Converts relative paths to absolute paths (the standard Bundle Transformer feature, implemented as a postprocessor).
  2. AutoprefixCssPostProcessor. Adds Autoprefix support to Bundle Transformer.

At first glance, this is very similar to the registration of minimizers, but there is one small difference: if we can specify only one minimizer in the defaultMinifier attribute of the /configuration/bundleTransformer/core/css element, then we can specify any number of postprocessors in the defaultPostProcessors attribute (even zero). ). Moreover, the order in which we specify the names of postprocessors in this attribute determines the order of their execution. If the attribute is missing, then the default UrlRewritingCssPostProcessor used as the postprocessor.

The code also shows that the value of the useInDebugMode attribute of postprocessors differs: in UrlRewritingCssPostProcessor it is false (conversion of relative paths to absolute is needed only in release mode, when all files are merged into one), and in AutoprefixCssPostProcessor - true (updating vendor prefixes is necessary and in debugging mode and release mode).

Registering JavaScript postprocessors is almost the same as registering CSS postprocessors, except that it must be done in the configuration element /configuration/bundleTransformer/core/js .

Advanced HTTP debug handlers


As a rule, most users of Bundle Transformer configure modules in a declarative way (via Web.config ), but in some cases it is necessary to use the imperative approach. For example, when working with LESS variables:

 using System.Collections.Generic; using System.Web.Optimization; using BundleTransformer.Core.Builders; using BundleTransformer.Core.Orderers; using BundleTransformer.Core.Transformers; using BundleTransformer.Core.Translators; using BundleTransformer.Less.Translators; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { var nullBuilder = new NullBuilder(); var nullOrderer = new NullOrderer(); var lessTranslator = new LessTranslator { GlobalVariables = "my-variable='Hurrah!'", ModifyVariables = "font-family-base='Comic Sans MS';body-bg=lime;font-size-h1=50px" }; var styleTransformer = new StyleTransformer( new List<ITranslator> { lessTranslator }); var commonStylesBundle = new Bundle("~/Bundles/BootstrapStyles"); commonStylesBundle.Include("~/Content/bootstrap/bootstrap.less"); commonStylesBundle.Builder = nullBuilder; commonStylesBundle.Transforms.Add(styleTransformer); commonStylesBundle.Orderer = nullOrderer; bundles.Add(commonStylesBundle); } } 

In the above code, we explicitly create an instance of the LessTranslator class and use the GlobalVariables and ModifyVariables to configure LESS variables. With this approach, we can transmit to the translator the values ​​of LESS variables obtained from an external source (for example, a database).

There is a second way to work with LESS variables. First you need to create a custom element transformation:

 using System.Text; using System.Web.Optimization; public sealed class InjectContentItemTransform : IItemTransform { private readonly string _beforeContent; private readonly string _afterContent; public InjectContentItemTransform(string beforeContent, string afterContent) { _beforeContent = beforeContent ?? string.Empty; _afterContent = afterContent ?? string.Empty; } public string Process(string includedVirtualPath, string input) { if (_beforeContent.Length == 0 && _afterContent.Length == 0) { return input; } var contentBuilder = new StringBuilder(); if (_beforeContent.Length > 0) { contentBuilder.AppendLine(_beforeContent); } contentBuilder.AppendLine(input); if (_afterContent.Length > 0) { contentBuilder.AppendLine(_afterContent); } return contentBuilder.ToString(); } } 

And then register it when adding a file to the bundle:

 using System.Web.Optimization; using BundleTransformer.Core.Bundles; using BundleTransformer.Core.Orderers; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { var nullOrderer = new NullOrderer(); const string beforeLessCodeToInject = @"@my-variable: 'Hurrah!';"; const string afterLessCodeToInject = @"@font-family-base: 'Comic Sans MS'; @body-bg: lime; @font-size-h1: 50px;"; var commonStylesBundle = new CustomStyleBundle("~/Bundles/BootstrapStyles"); commonStylesBundle.Include( "~/Content/bootstrap/bootstrap.less", new InjectContentItemTransform(beforeLessCodeToInject, afterLessCodeToInject)); commonStylesBundle.Orderer = nullOrderer; bundles.Add(commonStylesBundle); } } 

Unfortunately, the above code examples previously only worked in release mode. This was due to the fact that the HTTP debug handlers "did not know anything" about the settings of the bundles and simply translated the code of the requested files.

To solve this problem, it was necessary, first of all, to find a way to transfer debug HTTP handlers to the URL of the bundle, which includes the requested file. After examining the System.Web.Optimization.dll assembly code using the decompiler, the solution was found: you need to write your own version of the BundleResolver class and register it in the appropriate class. I will not go into the implementation details, but just show you how to use the created class:

 … using BundleTransformer.Core.Resolvers; public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { BundleResolver.Current = new CustomBundleResolver(); … } } 

After that, in debug mode, links to the following files will be generated:

 <link href="/Content/bootstrap/bootstrap.less?bundleVirtualPath=%7e%2fBundles%2fBootstrapStyles" rel="stylesheet"> 

where the query string parameter bundleVirtualPath contains the URL of the bundle.

Thus, with the bandl URL, I added to the basic debug HTTP-handler the ability to apply custom element transformations and transformations set at the bundle level (translators and postprocessors) to the requested file.

In addition, two additional HTTP handlers were created:
  1. CssAssetHandler. To handle CSS files.
  2. JsAssetHandler. To handle javascript files.

They allow you to apply custom element transformations and postprocessors to static files. If the requested static file is not included in any of the bundles, then these HTTP handlers pass the request to an instance of the System.Web.StaticFileHandler class. In contrast to the HTTP debugger henders bundled with translators, these HTTP handlers are not registered in the Web.config file automatically (during the installation of NuGet packages), they must be registered manually:

 <configuration> … <system.webServer> … <handlers> … <add name="CssAssetHandler" path="*.css" verb="GET" type="BundleTransformer.Core.HttpHandlers.CssAssetHandler, BundleTransformer.Core" resourceType="File" preCondition="" /> <add name="JsAssetHandler" path="*.js" verb="GET" type="BundleTransformer.Core.HttpHandlers.JsAssetHandler, BundleTransformer.Core" resourceType="File" preCondition="" /> … </handlers> … </system.webServer> … </configuration> 

Mapping File Extensions and Resource Types in Web.config


Previously, the mapping of file extensions and resource types was hard-wired in the Asset class code. Now the configuration elements fileExtensions from the Web.config file are used for this:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> <css …> … <fileExtensions> <add fileExtension=".css" assetTypeCode="Css" /> <add fileExtension=".less" assetTypeCode="Less" /> <add fileExtension=".sass" assetTypeCode="Sass" /> <add fileExtension=".scss" assetTypeCode="Scss" /> </fileExtensions> </css> <js …> … <fileExtensions> <add fileExtension=".js" assetTypeCode="JavaScript" /> <add fileExtension=".coffee" assetTypeCode="CoffeeScript" /> <add fileExtension=".litcoffee" assetTypeCode="LiterateCoffeeScript" /> <add fileExtension=".coffee.md" assetTypeCode="LiterateCoffeeScript" /> <add fileExtension=".ts" assetTypeCode="TypeScript" /> <add fileExtension=".mustache" assetTypeCode="Mustache" /> <add fileExtension=".handlebars" assetTypeCode="Handlebars" /> <add fileExtension=".hbs" assetTypeCode="Handlebars" /> </fileExtensions> </js> … </core> … </bundleTransformer> … </configuration> 

The example above shows the situation when all official Bundle Transformer modules are installed (comparisons for the .css and .js extensions are added during the kernel installation, and the rest when the corresponding translator modules are installed). This architecture gives us the following advantages:
  1. No need to store unused mappings. As a rule, in real projects it is not necessary to install all types of translators (for example, the simultaneous use of LESS and Sass is very rare), thus fewer comparisons will be stored in the project.
  2. The ability to create informal modules-translators. Since now there is no dependence on the kernel code, users of Bundle Transformer have the opportunity to create their own module-translators. An example of such a module is the AngularBundle NuGet package, which , when installed, adds the following mapping to the Web.config file:

     <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core> … <js …> … <fileExtensions> … <add fileExtension=".html" assetTypeCode="AngularTemplate" /> … </fileExtensions> </js> … </core> … </bundleTransformer> … </configuration> 

  3. Binding of new file extensions to existing modules-translators. For example, if we want the BundleTransformer.Hogan module to start processing files with the .html extension, we just need to add the following code to the Web.config file:

     <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core …> … <js …> … <fileExtensions> … <add fileExtension=".html" assetTypeCode="Mustache" /> … </fileExtensions> </js> … </core> … </bundleTransformer> … <system.webServer> … <handlers> … <add name="HtmlAssetHandler" path="*.html" verb="GET" type="BundleTransformer.Hogan.HttpHandlers.HoganAssetHandler, BundleTransformer.Hogan" resourceType="File" preCondition="" /> … </handlers> … </system.webServer> … </configuration> 

Merging code files before minimizing


The Bundle Transformer, unlike System.Web.Optimization, processes each file separately and this approach offers several advantages:
  1. It becomes possible to combine different types of resources into one bundle (for example, CSS-, LESS- and Sass-files).
  2. The minimization of previously minimized files (files with the extensions .min.css and .min.js ) is not .min.js , which in most cases increases the speed of minimization during the initial access to the bundle.

But some users of Bundle Transformer did not like this approach, because they wanted to take full advantage of the structural minimization features that modern minimizers provide (for example, CSSO from Yandex).

Therefore, in the new version, the css and js configuration elements have the combineFilesBeforeMinification attribute (the default value is false ), which allows you to enable the merging of the file code before minimizing:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> <core <css … combineFilesBeforeMinification="true"> … </css> <js … combineFilesBeforeMinification="true"> … </js> … </core> … </bundleTransformer> … </configuration> 

New modules


During this time, three official modules were created for Bundle Transformer at once:
  1. Postprocessor BundleTransformer.Autoprefixer
  2. Translator BundleTransformer.Handlebars
  3. Translator BundleTransformer.Hogan

Since all three modules are based on the code of JavaScript libraries, immediately after installation you need to select your own JavaScript engine for each of them (for more information, see the readme.txt files of the corresponding NuGet packages).

Consider each of them separately:

Bundle Transformer: Autoprefixer


The BundleTransformer.Autoprefixer module contains the AutoprefixCssPostProcessor postprocessor AutoprefixCssPostProcessor , which updates the vendor prefixes in the CSS code. AutoprefixCssPostProcessor is based on the popular CSS postprocessor - Autoprefixer (version 3.1 is currently supported). I will not explain why Autoprefixer is needed, because you can emphasize all the basic information about this product from Andrei Sitnik ’s article “Autoprefixer - the final solution to the problem of prefixes in CSS” .

In this section, I will discuss how to properly configure BundleTransformer.Autoprefixer. If you have not read the Postprocessors and the Advanced HTTP Debug Handlers sections of this article, be sure to read them, since they address many important points related to the work of BundleTransformer.Autoprefixer.

After installing BundleTransformer.Autoprefixer and selecting the JavaScript engine, you need to perform the following steps:
  1. Add the AutoprefixCssPostProcessor postprocessor to the end of the list of active CSS postprocessors, which is specified in the defaultPostProcessors attribute of the configuration element /configuration/bundleTransformer/core/css .
  2. Register the CssAssetHandler HTTP debug handler in the Web.config file (required for debug mode).
  3. Register an instance of the CustomBundleResolver class as the current BundleResolver `(required for debug mode).

Then, in the configuration section /configuration/bundleTransformer/autoprefixer the Web.config file, you can make the optional settings of the Autoprefixer algorithm:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <autoprefixer cascade="true" safe="false"> <browsers> <add conditionalExpression="> 1%" /> <add conditionalExpression="last 2 versions" /> <add conditionalExpression="Firefox ESR" /> <add conditionalExpression="Opera 12.1" /> </browsers> … </autoprefixer> … </bundleTransformer> … </configuration> 

Consider in detail all the properties of the configuration section autoprefixer :
PropertyData typeDefault valueDescription
browsersList of conditional expressions1%
last 2 versions ,
Firefox ESR
Opera 12.1
Contains a list of conditional expressions to define a subset of supported browsers. The syntax of conditional expressions is described in detail in the official documentation of Autoprefixer . If the browsers element is not specified or is empty, the default value is used. To completely disable the addition of vendor prefixes, you need to leave only one conditional expression equal to none in the browsers element.
cascadeBooleantrueCreates a visual cascade of the following prefixes:

 -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; 
safeBooleanfalseIncludes special safe mode for parsing broken CSS code.

Bundle Transformer: Handlebars


The BundleTransformer.Handlebars module contains the HandlebarsTranslator adapter-translator, which precompiles Handlebars templates in JavaScript. HandlebarsTranslator is based on the popular template engine - Handlebars.js (version 2.0.0 is currently supported). Despite the fact that this translator is based on a template engine, it is not much different from other translators that produce JavaScript code as output. Files with template code (by default, the translator processes files with the .handlebars and .hbs ) must be registered in the script bundles:

 … using Core.Bundles; using Core.Orderers; … public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { … var commonTemplatesBundle = new CustomScriptBundle("~/Bundles/CommonTemplates"); commonTemplatesBundle.Include( … "~/Scripts/handlebars/handlebars.runtime.js", "~/Scripts/handlebars/HandlebarsHelpers.js", "~/Scripts/handlebars/HandlebarsTranslatorBadge.handlebars", …); commonTemplatesBundle.Orderer = nullOrderer; bundles.Add(commonTemplatesBundle); … } } 

Unlike CoffeeScript and TypeScript, compiled Handlebars templates require the file handlebars.runtime.js (a stripped-down version of the library handlebars.js , from which the code necessary for compiling templates is excluded). This file can be placed in a shared library bundle or in a bundle with Handlebars templates. The main thing that his ad went before the announcement of templates.

In the configuration section /configuration/bundleTransformer/handlebars the Web.config file, you can configure the precompilation of templates:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <handlebars namespace="Handlebars.templates" rootPath="" knownHelpers="link" knownHelpersOnly="true" data="false"> … </handlebars> … </bundleTransformer> … </configuration> 

Consider in detail all the properties of the configuration section of handlebars :
PropertyData typeDefault valueDescription
namespaceLineHandlebars.templatesSets the namespace for templates.
rootPathLineEmpty lineSets the path to the template root directory. Suppose that we have the following template URL, /Scripts/handlebars/discussions/index.hbs . By default, the name of such a template is extracted from the file name - index , but if we assign this property a value of /Scripts/handlebars/ , then we get the following template name - discussions/index .
knownHelpersLineEmpty lineContains a comma-separated list of known helpers. Adding helper names to this list allows you to optimize the calls to them, which reduces the size of the compiled template.
knownHelpersOnlyBooleanfalseAllows the use of only known helpers. If the value of this property is true and the template code contains helper calls that are not embedded or not declared in the knownHelpers property, an exception will be thrown.
dataBooleantrueAllows compilation to include in the data template for @ data-variables (for example, @index ). If there are iterative blocks in your templates, but @ data variables are not used, it is better to set this property a value equal to false - this will increase performance.

It is also worth noting that if the name of the file containing the template code begins with an underscore, the template will be compiled as a global partial representation (the initial underscore will be removed from the template name).

Bundle Transformer: Hogan


The BundleTransformer.Hogan module contains the HoganTranslator adapter-translator, which precompiles Mustache templates in JavaScript. HoganTranslator is based on the popular Mustache template compiler - Hogan.js (version 3.0.2 is currently supported). The principles of BundleTransformer.Hogan work in many ways similar to the principles of BundleTransformer.Handlebars, so we consider only the key differences. Files with template code (by default, the translator processes files with the .mustache extension) must be registered in the script bundles:

 … using Core.Bundles; using Core.Orderers; … public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { … var commonTemplatesBundle = new CustomScriptBundle("~/Bundles/CommonTemplates"); commonTemplatesBundle.Include( "~/Scripts/hogan/template-{version}.js", "~/Scripts/hogan/HoganTranslatorBadge.mustache", …); commonTemplatesBundle.Orderer = nullOrderer; bundles.Add(commonTemplatesBundle); … } } 

As in Handlebars, compiled templates require a special JavaScript library - template-3.0.2.js(in future releases, the version number is likely to change).

In the configuration section of the /configuration/bundleTransformer/hoganfile, Web.configyou can configure precompiling templates:

 <configuration> … <bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd"> … <hogan useNativeMinification="false" variable="templates" namespace="" delimiters=""> <sectionTags> <add sectionName="newWindow" openingTagName="_newWindow" closingTagName="newWindow" /> </sectionTags> … </hogan> … </bundleTransformer> … </configuration> 

Consider in detail all the properties of the configuration section hogan:
PropertyData typeDefault valueDescription
useNativeMinificationBooleanfalseIf the value of this property is equal true, then the minimization of the code of the compiled template will be performed by means of the translator.
variableLinetemplatesSpecifies the name of the JavaScript variable in which the templates will be stored.
namespaceLineEmpty lineSpecifies the namespace that is prefixed to the name of the template.
sectionTagsCustom Tag ListBlank list, . , _newWindow newWindow , {{_newWindow}} target="_blank"{{/newWindow}} .
delimitersLineEmpty line, . , ASP, – <% %> ( Web.config — &lt;% %&gt; ).

Source: https://habr.com/ru/post/237743/


All Articles