91

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.

coumarc9
  • 7
  • 2
jrummell
  • 42,637
  • 17
  • 112
  • 171

7 Answers7

116

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")
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 11
    This should work, but it also should be unnecessary, as the default orderer generally respects the order of the includes (it only promotes a few special files to the top, i.e. jquery, reset.css/normalize.css, etc.). It should not be promoting achievements.js to the top. – Hao Kung Aug 16 '12 at 21:48
  • @HaoKung I find the default orderer and even this AsIsBundleOrderer consistently promotes my custom Site.js before jquery-x.js. Where can I submit my repro? – flipdoubt Nov 24 '12 at 20:46
  • 1
    @flipdoubt file an issue here with the repro: http://aspnetoptimization.codeplex.com/workitem/list/advanced – Hao Kung Dec 06 '12 at 19:33
  • +1 I'm using RTM and just ran into the problem when adding all of my scripts to a single bundle. And to add to this, this interface is in System.Web.Optimization as described here: http://msdn.microsoft.com/en-us/library/system.web.optimization.ibundleorderer.aspx – Brian Mains Mar 23 '13 at 14:34
  • 1
    This works perfect, thanks! I agree though, this should not be necessary. – Chris Barr Apr 15 '13 at 16:07
  • 2
    This works for me. It was promoting jqueryui over bootstrap when according to http://stackoverflow.com/questions/17367736/jquery-ui-dialog-missing-close-icon I need it flipped. – Sethcran Mar 11 '14 at 15:25
  • If the file set contains files in subdirectories, the files in the subdirectories get chunked first, instead of the order that they were processed in. Example: "~/Scripts/angular.js", "~/Scripts/angular-route.min.js", "~/Scripts/angular-touch.js", "~/Scripts/angular-bootstrap-switch.min.js", "~/Scripts/ui-bootstrap-tpls-0.10.0.min.js", "~/Scripts/acs/controllers.js", "~/Scripts/acs/app.js", "~/Scripts/acs/slider.js" controllers.js appears before app.js – Michael Draper Aug 20 '14 at 19:55
  • @HaoKung You're right, however if you do not use the exact naming scheme they have, you'll end up having issues (example: using . instead of - in file name for the min. jquery-min.js vs jquery.min.js) could cause you some trouble trying to get the order you're looking for. The above solution gives back full control of the ordering. – Jessy Oct 24 '14 at 21:53
  • I am getting this error: Error 64 'AsIsBundleOrderer' does not implement interface member 'System.Web.Optimization.IBundleOrderer.OrderFiles(System.Web.Optimization.BundleContext, System.Collections.Generic.IEnumerable)' – Liron Harel Jul 16 '15 at 02:24
  • 1
    @Hao Kung can we order the files in the [include](http://stackoverflow.com/a/19894495/2218697) directory ? – Shaiju T Jan 18 '16 at 08:14
  • Signature has changed to public IEnumerable OrderFiles(BundleContext context, IEnumerable files) – Rajshekar Reddy Feb 01 '16 at 08:37
52

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());
Softlion
  • 12,281
  • 11
  • 58
  • 88
  • 9
    Pretty slick! :D Right now I had to change `FileInfo` for `BundleFile` in the Interface method signature. They changed it. – Leniel Maccaferri Jul 04 '13 at 20:34
  • yes they changed it in the prerelease version which uses the Owin framework – Softlion Sep 01 '13 at 20:08
  • little Off-Topic: does anybody have more info on this `BundleFile` class? In order to bundle embedded resources, I'm trying to instantiate that and it requires a (concrete child of) `VirtualFile` parameter in its constructor. – superjos Sep 13 '13 at 09:27
  • I have the same question, I need instantiate BundleFile, but don't know how – Rui Sep 26 '13 at 06:33
  • 2
    @superjos I know this is rather old but I'll answer in case anyone else has the problem. It's part of System.Web.Optimization. – Talon May 11 '15 at 07:41
  • this was absolutely the answer I was looking for. – Ian Apr 23 '18 at 18:06
47

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.

Gerald Davis
  • 4,541
  • 2
  • 31
  • 47
18

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...

jrummell
  • 42,637
  • 17
  • 112
  • 171
Hao Kung
  • 28,040
  • 6
  • 84
  • 93
  • I upgraded to the 1.0 version. See my updated question. The contents of achievements.js are included first in the minified version, but it works since its a function called later. – jrummell Aug 17 '12 at 02:23
5

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);
Arne H. Bitubekk
  • 2,963
  • 1
  • 27
  • 34
1

@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)
Chris Barr
  • 29,851
  • 23
  • 95
  • 135
  • I keep getting this error: `Class 'AsIsBundleOrderer' must implement 'Function OrderFiles(context as ....)' for interface 'System.Web.Optimization.IBundleOrderer'` Any ideas? – Mark Pieszak - Trilon.io Feb 24 '14 at 21:42
1

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/

Ori Calvo
  • 426
  • 5
  • 6