How can I specify an explicit ScriptBundle include order?

66.3k Views Asked by At

I'm trying out the MVC4 System.Web.Optimization 1.0 ScriptBundle feature.

I have the following configuration:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        // shared scripts
        Bundle canvasScripts =
            new ScriptBundle(BundlePaths.CanvasScripts)
                .Include("~/Scripts/modernizr-*")
                .Include("~/Scripts/json2.js")
                .Include("~/Scripts/columnizer.js")
                .Include("~/Scripts/jquery.ui.message.min.js")
                .Include("~/Scripts/Shared/achievements.js")
                .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(canvasScripts);
    }
}

and the following view:

<script type="text/javascript" src="@Scripts.Url(BundlePaths.CanvasScripts)"></script>

where BundlePaths.CanvasScripts is "~/bundles/scripts/canvas". It renders this:

<script type="text/javascript" src="/bundles/scripts/canvas?v=UTH3XqH0UXWjJzi-gtX03eU183BJNpFNg8anioG14_41"></script>

So far so good, except ~/Scripts/Shared/achievements.js is the first script in the bundled source. It depends on every script included before it in the ScriptBundle. How can I ensure that it honors the order in which I add include statements to the bundle?

Update

This was a relatively new ASP.NET MVC 4 application, but it was referencing the optimization framework pre release package. I removed it and added the RTM package from http://nuget.org/packages/Microsoft.AspNet.Web.Optimization. With the RTM version with debug=true in web.config, @Scripts.Render("~/bundles/scripts/canvas") renders the individual script tags in the correct order.

With debug=false in web.config, the combined script has the achievements.js script first, but since its a function definition (object constructor) that's called later, it runs without error. Perhaps the minifier is smart enough to figure out dependencies?

I also tried the IBundleOrderer implementation that Darin Dimitrov suggested with RTM with both debug options and it behaved the same.

So the minified version is not in the order I expect, but it works.

7

There are 7 best solutions below

1
On BEST ANSWER

I'm not seeing this behavior on the RTM bits, are you using the Microsoft ASP.NET Web Optimization Framework 1.0.0 bits: http://nuget.org/packages/Microsoft.AspNet.Web.Optimization ?

I used a similar repro to your sample, based off of a new MVC4 Internet application website.

I added to BundleConfig.RegisterBundles:

        Bundle canvasScripts =
            new ScriptBundle("~/bundles/scripts/canvas")
                .Include("~/Scripts/modernizr-*")
                .Include("~/Scripts/Shared/achievements.js")
                .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(canvasScripts); 

And then in the default index page, I added:

<script src="@Scripts.Url("~/bundles/scripts/canvas")"></script>

And I verified that in the minified javascript for the bundle, the contents of achievements.js was after modernizr...

0
On

You should consider using Cassette http://getcassette.net/ It supports auto detection of script dependencies based on script references found inside each file.

If you prefer sticking to MS Web Optimization solution but like the idea of arranging scripts based on script references you should read my blog post at http://blogs.microsoft.co.il/oric/2013/12/27/building-single-page-application-bundle-orderer/

11
On

You could write a custom bundle orderer (IBundleOrderer) that will ensure bundles are included in the order you register them:

public class AsIsBundleOrderer : IBundleOrderer
{
    public virtual IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
    {
        return files;
    }
}

and then:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        var bundle = new Bundle("~/bundles/scripts/canvas");
        bundle.Orderer = new AsIsBundleOrderer();
        bundle
            .Include("~/Scripts/modernizr-*")
            .Include("~/Scripts/json2.js")
            .Include("~/Scripts/columnizer.js")
            .Include("~/Scripts/jquery.ui.message.min.js")
            .Include("~/Scripts/Shared/achievements.js")
            .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(bundle);
    }
}

and in your view:

@Scripts.Render("~/bundles/scripts/canvas")
6
On

Thank you Darin. I've added an extension method.

internal class AsIsBundleOrderer : IBundleOrderer
{
    public virtual IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
    {
        return files;
    }
}

internal static class BundleExtensions
{
    public static Bundle ForceOrdered(this Bundle sb)
    {
        sb.Orderer = new AsIsBundleOrderer();
        return sb;
    }
}

Usage

bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js",
                    "~/Scripts/jquery-migrate-{version}.js",

                    "~/Scripts/jquery.validate.js",
                    "~/Scripts/jquery.validate.messages_fr.js",
                    "~/Scripts/moon.jquery.validation-{version}.js",

                    "~/Scripts/jquery-ui-{version}.js"
                    ).ForceOrdered());
3
On

Updated the answer provided by SoftLion to handle changes in MVC 5 (BundleFile vs FileInfo).

internal class AsIsBundleOrderer : IBundleOrderer
{
    public virtual IEnumerable<BundleFile> OrderFiles(BundleContext context, IEnumerable<BundleFile> files)
    {
        return files;
    }
}

internal static class BundleExtensions
{
    public static Bundle ForceOrdered(this Bundle sb)
    {
        sb.Orderer = new AsIsBundleOrderer();
        return sb;
    }
}

Usage:

    bundles.Add(new ScriptBundle("~/content/js/site")
        .Include("~/content/scripts/jquery-{version}.js")
        .Include("~/content/scripts/bootstrap-{version}.js")
        .Include("~/content/scripts/jquery.validate-{version}")
        .ForceOrdered());

I like using fluent syntax but it also works with a single method call and all the scripts passed as parameters.

0
On

You should be able to set the order with help of the BundleCollection.FileSetOrderList. Have a look at this blog post: http://weblogs.asp.net/imranbaloch/archive/2012/09/30/hidden-options-of-asp-net-bundling-and-minification.aspx . The code in your instance would be something like:

BundleFileSetOrdering bundleFileSetOrdering = new BundleFileSetOrdering("js");
bundleFileSetOrdering.Files.Add("~/Scripts/modernizr-*");
bundleFileSetOrdering.Files.Add("~/Scripts/json2.js");
bundleFileSetOrdering.Files.Add("~/Scripts/columnizer.js");
bundleFileSetOrdering.Files.Add("~/Scripts/jquery.ui.message.min.js");
bundleFileSetOrdering.Files.Add("~/Scripts/Shared/achievements.js");
bundleFileSetOrdering.Files.Add("~/Scripts/Shared/canvas.js");
bundles.FileSetOrderList.Add(bundleFileSetOrdering);
1
On

@Darin Dimitrov's answer works perfectly for me, but my project is written in VB, so here's his answer converted to VB

Public Class AsIsBundleOrderer
    Implements IBundleOrderer

    Public Function IBundleOrderer_OrderFiles(ByVal context As BundleContext, ByVal files As IEnumerable(Of FileInfo)) As IEnumerable(Of FileInfo) Implements IBundleOrderer.OrderFiles
        Return files
    End Function
End Class

To Use it:

Dim scriptBundleMain = New ScriptBundle("~/Scripts/myBundle")
scriptBundleMain.Orderer = New AsIsBundleOrderer()
scriptBundleMain.Include(
    "~/Scripts/file1.js",
    "~/Scripts/file2.js"
)
bundles.Add(scriptBundleMain)