JavaScript Bundling (Alpha version)

We want to introduce an optional JavaScript bundling setup for projects based on SCORE and SCORE's project structure. JavaScript bundling reduces count of requests to the web-server and improve caching, since js-bundle is a static file which is generated at the build time.

This solution is only one option you can consider for your SCORE implementation - we suggest to use the RequireJS optimizer, also known as r.js more about it here: http://requirejs.org/docs/optimization.html.

Pre-requirements

  • SCORE UI 2.1.27.0 or higher.
  • SCORE Bootstrap UI 2.1.23.0 or higher.
  • NodeJS v0.10.36 or higher (https://nodejs.org/)
  • Powershell 4 or higher

Installation

  1. Unzip js-minify_v20160930.zip into the root of the solution folder

  2. Update all your require.config.js from the SCORE solution template to the format as described here:
    Wrap your module definition into an anonymous function and pass window as a parameter called global. Replace all references to window with this global parameter.

  3. Remove global.require.baseUrl parameter. While bundling each module receives its own unique name, which is based on the path to the module. And only by that name the module will be available. We can't rely on baseUrl since it doesn't work with bundling.

  4. Make sure all custom components are registered with alias: my_site/Components/myCustomComponent. If your project doesn't follow this general rule, you need to modify all custom components and add alias to your path when calling Html.BeginUXModule helper.

    ;(function(global) {
        global.require = global.require || {};
        //global.require.baseUrl = "/Areas/MySite/js";
    
        global.require.paths = global.require.paths || {};
        global.require.paths.my_site                    = "/Areas/MySite/js";
        global.require.paths.myCustomComponent             = global.require.paths.my_site + "/Components/myCustomComponent";
    
        global.require.shim = global.require.shim || {};
        global.require.shim.bootstrap                   = { deps: ["jquery"] };
        global.require.shim.matchHeight                 = { deps: ["jquery"] };
        global.require.shim.jqueryValidate              = { deps: ["jquery"] };
        global.require.shim.jqueryUnobtrusiveAjax       = { deps: ["jquery", "jqueryValidate", "jqueryValidateUnobtrusive"] };
        global.require.shim.jqueryValidateUnobtrusive   = { deps: ["jqueryValidate"] };
        global.require.shim.scorevalidation             = { deps: ["jqueryUnobtrusiveAjax", "jqueryValidateUnobtrusive"] };
    })(this /* window */);
    Before modification
    window.require = window.require || {};
    window.require.baseUrl = "/Areas/MySite/js";
    
    window.require.paths                           = window.require.paths || {};
    window.require.paths.my_site                   = "/Areas/MySite/js";
    window.require.paths.myCustomComponent            = window.require.paths.my_site + "/Components/myCustomComponent";
    
    window.require.shim = window.require.shim || {};
    window.require.shim.bootstrap                 = { deps: ["jquery"] };
    window.require.shim.matchHeight               = { deps: ["jquery"] };
    window.require.shim.jqueryValidate            = { deps: ["jquery"] };
    window.require.shim.jqueryUnobtrusiveAjax     = { deps: ["jquery", "jqueryValidate", "jqueryValidateUnobtrusive"] };
    window.require.shim.jqueryValidateUnobtrusive = { deps: ["jqueryValidate"] };
    window.require.shim.scorevalidation           = { deps: ["jqueryUnobtrusiveAjax", "jqueryValidateUnobtrusive"] };
    
  5. Modify <solution root>\MySolution.MySite.Web\Areas\MySite\Views\Layouts\Main.cshtml to load only one configuration file: MySite.bundle.config.js

    <!DOCTYPE html>
    <head>
        <!-- ........ -->
    </head>
    <body>
        <!-- ........ -->
    
        <!-- Placed at the end of the document so the pages load faster -->
        <script src="@Url.Content("~/Areas/MySite/js/MySite.bundle.config.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Areas/ScoreCCF/js/Vendor/require.js")" data-main="/Areas/MySite/js/main.js" type="text/javascript"></script>
    
        <!-- ........ -->
    </body>
    </html>
    Before modification
    <!DOCTYPE html>
    <head>
        <!-- ........ -->
    </head>
    <body>
        <!-- ........ -->
    
        <!-- Placed at the end of the document so the pages load faster -->
        @foreach (var area in new[] { "ScoreCCF", "ScoreBootstrapUI", "MySite" })
        {
            <script src="@Url.Content("~/Areas/" + area + "/js/require.config.js")" type="text/javascript"></script>
        }
        <script src="@Url.Content("~/Areas/ScoreCCF/js/Vendor/require.js")" data-main="/Areas/MySite/js/main.js" type="text/javascript"></script>
    
        <!-- ........ -->
    </body>
    </html>
  6. Modify the \MySolution.MySite.Web\MySolution.MySite.Web.csproj to generate MySite.bundle.js and MySite.bundle.config.js at build time.
    Update /Project/Target[@name="CompileLess" or @name="CompileSass"] with one more command:

    <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <!-- ....... -->
      <Target Name="CompileSass" AfterTargets="Build">
        <Exec Command="sass Areas\MySite\scss\main.scss Areas\MySite\css\main.css  --scss --sourcemap=auto" IgnoreExitCode="false" />
        <Exec Command="sass Areas\MySite\scss\page_editor.scss Areas\MySite\css\page_editor.css --scss --sourcemap=auto" IgnoreExitCode="false" />
        <Exec Command="powershell -ExecutionPolicy ByPass -file &quot;$(SolutionDir)Automation\js-minify\bundle.ps1&quot; -solutionPath &quot;$(SolutionDir)\&quot; -outFolderPath &quot;$(ProjectName)\Areas\MySite\js&quot; -areasPath &quot;$(ProjectName)\Areas\MySite&quot; -bundleName &quot;/Areas/MySite/js/MySite.bundle&quot; -nugetDeps &quot;Score.UI.*,Score.BootstrapUI.*&quot; -bundleFolder &quot;$(ProjectName).JsBundle&quot; -configuration &quot;$(Configuration)&quot;" IgnoreExitCode="false" />
      </Target>
      <!-- ....... -->
    </Project>
    Before modification
    <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <!-- ....... -->
      <Target Name="CompileSass" AfterTargets="Build">
        <Exec Command="sass Areas\MySite\scss\main.scss Areas\MySite\css\main.css  --scss --sourcemap=auto" IgnoreExitCode="false" />
        <Exec Command="sass Areas\MySite\scss\page_editor.scss Areas\MySite\css\page_editor.css --scss --sourcemap=auto" IgnoreExitCode="false" />
      </Target>
      <!-- ....... -->
    </Project>
    


Additional Information

Automation\js-minify\bundle.ps1 has the next parameters:

  • SolutionPath - (Required) Path to the solution folder
  • AreasPath (Required) Path to all Areas which should be added to the bundle. You can list more than one Area, you should use "," (comma). eg.: MySolution.MySite.Web\Areas\MySite,MySolution.MySite.Web\Areas\MySite2 (Use "\" separator char )
  • BundleFolder (Required) Folder where all scripts will be built. Created and Deleted automatically. if you generate more than one bundle on a build, you should use separated folders for each bundle.
  • BundleName - (Required) Filename. Since bundle name used by requirejs as a path to the file, you probably want to pass a path to the bundle file, eg.: /Areas/MySite/js/MySite.bundle (Use "/" separator char)
  • OutFolderPath - (Required) Path to the folder where the result js should be stored. eg.: MySolution.MySite.Web\Areas\MySite\js (Use "\" separator char)
  • Configuration - (Required) Current build configuration. For PROD-* will be enabled uglify2 optimization
  • Optimize - (Optional) rjs optimization level
  • NugetDeps - (Optional) Nuget dependencies. (IMPORTANT: nuget dependencies should be restored. Current version of the script supports only one solution template: it looking for JS files under: \packages\<packageName>\tools\Areas\*\js\. otherwise it'll be skipped.)

Default build configuration

Developer can modify and tweak r.js build configuration by editing the file \Automation\js-minify\bundle.rjs.default.config.js

({
    // More about configuration file you can read by the link: https://github.com/requirejs/r.js
    mainConfigFile: [],
    baseUrl: "./",
    out: './bundled.js',
    optimize: 'none',//'uglify2',
    uglify2: {
        // Exclude ModulesLoadedEvent from uglify, to keep ModulesLoadedEvent.name equals to "ModulesLoadedEvent" 
        // instead of uglified function name, because Function.prototype.name replaces our property value.
        mangle: {
            except: ['ModulesLoadedEvent']
        }
    },
    preserveLicenseComments: false,
    generateSourceMaps: true,
    paths: {
        // https://github.com/requirejs/r.js/blob/master/build/example.build.js
        // If a special value of "empty:" is used for the path value, then that
        // acts like mapping the path to an empty file. It allows the optimizer to
        // resolve the dependency to path, but then does not include it in the output.
        // Useful to map module names that are to resources on a CDN or other
        // http: URL when running in the browser and during an optimization that
        // file should be skipped because it has no dependencies.
    },
    onBuildRead: function (moduleName, path, contents) {
        console.log("Reading: ", moduleName, ", Path: ", path);
        return contents;
    },
    wrapShim: true,
    include: [
        // Manually included modules to the bundle.
        // If module is not defined within require.config.js or /js/Components folder, 
        // but you want to have it in bundle -  write it here.
    ]
})