Bundle Transformer: Summer Update

    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 virtually no revolutionary changes to the Bundle Transformer. The changes were mostly evolutionary: support was added for new versions of minimizers and translators (the most routine and difficult part of working on a project), minor bugs were fixed, and work was ongoing 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 Bundle Transformer users. Most of them were implemented in version 1.9.0 and subsequent summer updates. In this article we will consider the most significant of them:

    Classes StyleTransformer and ScriptTransformer


    Initially, classes CssTransformerand JsTransformerhave been chosen not entirely successful names, because the choice of names was carried out by analogy with classes CssMinifyand JsMinifyfrom System.Web.Optimization . When classes appeared in System.Web.Optimization StyleBundleand ScriptBundleit became clear that these names were worthless.

    I delayed the renaming of these classes for a long time, but when working on version 1.9.0 I decided to rename them all the same. Now they are called StyleTransformerand ScriptTransformer. Old classes CssTransformerand JsTransformerare still available in the kernel (implemented as a wrapper of new classes), but they are considered obsolete (labeled attribute Obsolete), 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 Bundle Transformer architecture is not suitable for implementing such modules. Vegard implemented the functionality of this module in the form of a style translator, which was supposed to run 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 in NuGet an unofficial version of the module - BundleTransformer.Autoprefixer.Unofficial , and I started work on the new Bundle Transformer architecture.

    A new type of modules was required, which were to be launched after the translators and before the minimizers, and the number and order of calling such modules should be determined by the developer. As a name for the new type of modules, I decided to use the term “postprocessors”, which was coined by Andrei Sitnik (if you do not know who Andrei Sitnik is or what postprocessors are, then I recommend that you listen to the 6th issue of the Frontflip podcast ).

    The postprocessor in the Bundle Transformer can be any class that implements an interface IPostProcessoror inherits a base class PostProcessorBasefrom a namespace BundleTransformer.Core.PostProcessors. Like other types of modules (adapters), postprocessors must be registered in a file Web.config. Consider the registration process using CSS postprocessors as an example:


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

    На первый взгляд, это очень похоже на регистрацию минимизаторов, но есть одно небольшое отличие: если в атрибуте defaultMinifier элемента /configuration/bundleTransformer/core/css мы можем указать только один минимизатор, то в атрибуте defaultPostProcessors мы можем задать любое количество постпроцессоров (даже нулевое). Причем порядок, в котором мы указываем имена постпроцессоров в этом атрибуте, определяет порядок их выполнения. Если атрибут отсутствует, то в качестве постпроцессора по умолчанию используется UrlRewritingCssPostProcessor.

    Из кода также видно, что значение атрибута useInDebugMode у постпроцессоров различается: у UrlRewritingCssPostProcessor оно равно false (преобразование относительных путей в абсолютные нужно только в режиме выпуска, когда все файлы объединяются в один), а у AutoprefixCssPostProcessortrue(Updating vendor prefixes is needed both in debug mode and in release mode).

    Registering JavaScript postprocessors is practically no different from registering CSS postprocessors, except that it must be done in the configuration element /configuration/bundleTransformer/core/js.

    Enhanced Debugging HTTP Handlers


    Typically, most Bundle Transformer users configure modules in a declarative way (via Web.config), but in some cases you have to use an 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 { 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 class LessTranslatorand use the properties GlobalVariablesand ModifyVariablesconfigure the LESS variables. With this approach, we can pass 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 the 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 used to work only in release mode. This was due to the fact that the debugging HTTP handlers did not "know" anything about the bundle settings and simply broadcast the code of the requested files.

    To solve this problem, it was necessary, first of all, to find a way to pass the URL of the bundle into which the requested file is included to the HTTP debugging handlers. After studying the assembly code System.Web.Optimization.dllusing the decompiler, a solution was found: you need to write your own version of the class BundleResolverand register it in the corresponding class. I will not go into the implementation details, but just show 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 files of the following type will be generated:


    where the query string parameter bundleVirtualPathcontains the bundle URL.

    Thus, having the bundle URL at my disposal, I added the ability to apply custom element transformations and transformations specified at the bundle level (translators and postprocessors) to the requested file in the base debugging HTTP handler.

    In addition, two additional HTTP handlers were created:
    1. CssAssetHandler. For processing CSS files.
    2. JsAssetHandler. For processing JavaScript files.

    They allow you to apply custom transformations of elements 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 the class instance System.Web.StaticFileHandler. In contrast to the debugging HTTP handlers bundled with translators, these HTTP handlers are not Web.configautomatically registered in the file (during the installation of NuGet packages), they must be registered manually:


    Match file extensions and resource types in a Web.config file


    Previously, mapping file extensions to resource types was hard coded in the class code Asset. Now the configuration elements fileExtensionsfrom the file are used for this Web.config:


    In the example above illustrates a situation when the modules are installed all official Bundle Transformer (comparison for extensions .cssand .jsadded when the nucleus, and the other at installation-relevant translators modules). Such an 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), so fewer comparisons will be stored in the project.
    2. Ability to create informal translator modules. Since now there is no dependence on the kernel code, Bundle Transformer users have the opportunity to create their own translator modules. An example of such a module is the AngularBundle NuGet package , when installed Web.config, the following mapping is added to the file :


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


    Combining file code before minimizing


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

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

    Therefore, in the new version in the configuration elements cssand jsappeared attribute combineFilesBeforeMinification(the default is false), which allows you to include the union of file code before minimization:


    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 (see the files of the readme.txtcorresponding NuGet packages for more information ).

    Let's consider each of them separately:

    Bundle transformer: autoprefixer


    The BundleTransformer.Autoprefixer module contains a postprocessor adapter AutoprefixCssPostProcessorthat updates vendor prefixes in CSS code. AutoprefixCssPostProcessorcreated based on the popular CSS post processor - 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 an article by Andrei Sitnik, "Autoprefixer - the final solution to the CSS prefix problem . "

    In this section, I will talk about how to properly configure BundleTransformer.Autoprefixer. If you have not read the sections "Postprocessors" and "Advanced debugging HTTP-handlers" of this article, then be sure to read them, because they touch on many important points related to the operation of the BundleTransformer.Autoprefixer.

    After installing BundleTransformer.Autoprefixer and choosing a JavaScript engine, you need to do the following:
    1. Add a postprocessor AutoprefixCssPostProcessorto the end of the list of active CSS postprocessors, which is set in the attribute of the defaultPostProcessorsconfiguration element /configuration/bundleTransformer/core/css.
    2. Register a debug HTTP handler CssAssetHandlerin a file Web.config(required for debug mode).
    3. Register the class instance CustomBundleResolveras the current one BundleResolver(required for debug mode).

    Then, in the configuration section of the /configuration/bundleTransformer/autoprefixerfile, Web.configyou can make optional settings for the Autoprefixer algorithm:


    We consider in detail all the properties of the configuration section autoprefixer:
    PropertyData typeDefault valueDescription
    browsersConditional Expression List1%,
    last 2 versions,
    Firefox ESR,
    Opera 12.1
    Contains a list of conditional expressions for defining a subset of supported browsers. The syntax of conditional expressions is described in detail in the official documentation of Autoprefixer . If the item is browsersnot specified or is empty, the default value is used. To completely disable the addition of vendor prefixes, you need to leave browsersonly one conditional expression in the element equal none.
    cascadeBooleantrueCreates a visual cascade of prefixes of the following form:

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

    Bundle transformer: handlebars


    The BundleTransformer.Handlebars module contains a translator adapter HandlebarsTranslatorthat precompiles Handlebars templates into JavaScript. HandlebarsTranslatorcreated 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. Files with template code (by default, the translator processes files with extensions .handlebarsand .hbs) must be registered in 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 a file handlebars.runtime.js(a stripped down version of the library handlebars.jsfrom which the code necessary to compile the templates is excluded). This file can be placed in a bundle with shared libraries or in a bundle with Handlebars templates. The main thing is that his ad goes before the announcement of the templates.

    In the configuration section of the /configuration/bundleTransformer/handlebarsfile, Web.configyou can configure precompilation of templates:


    We consider in detail all the properties of the configuration section handlebars:
    PropertyData typeDefault valueDescription
    namespaceLineHandlebars.templatesSpecifies the namespace for the templates.
    rootPathLineEmpty lineSpecifies the path to the root directory of the templates. Suppose 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 set this property to equal /Scripts/handlebars/, we get the following template name - discussions/index.
    knownHelpersLineEmpty lineContains a comma-separated list of known helpers. Adding helpers names to this list allows optimizing calls to them, which leads to a decrease in the size of the compiled template.
    knownHelpersOnlyBooleanfalseAllows the use of only known helpers. If the value of this property is equal trueand the template code contains calls to helpers that are not built-in or not declared in the property knownHelpers, an exception will be thrown.
    dataBooleantrueAllows compilation to include data for @ data variables in templates (for example, @index). If your templates have iterative blocks, but @ data variables are not used, then it is better to set this property to equal false- this will increase productivity.

    It is also worth noting that if the name of the file containing the template code starts 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 a translator adapter HoganTranslatorthat pre-compiles Mustache templates in JavaScript. HoganTranslatorcreated based on the popular compiler of Mustache templates - Hogan.js (version 3.0.2 is currently supported). The operating principles of BundleTransformer.Hogan are in many respects similar to the operating principles of BundleTransformer.Handlebars, therefore we will consider only key differences. Files with template code (by default, files with the extension are processed by the translator .mustache) must be registered in 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 precompilation of templates:


    We 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 done 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 added as a prefix to the template name.
    sectionTagsCustom Tag ListEmpty listContains a list of user tags that will be processed as sections. For example, if we add a custom section with the name of the tag to be torn off _newWindowand the name of the tag to be closed newWindow, the code {{_newWindow}} target="_blank"{{/newWindow}}will be processed without errors by the compiler.
    delimitersLineEmpty lineSpecifies a string that overrides standard delimiters. For example, if we want to replace the standard delimiters in the form of double curly brackets with ASP-style delimiters, then this property will need to be assigned the following value - <% %>(when added to a file Web.config, special characters must be escaped - <% %>).


    Also popular now: