17

I've just chased a bug that was due to a missing javascript file, it was failing silently.

The minified version of the file was present but not the full version, a link isn't rendered on the client (which I was expecting) but I don't get an exception either. I'd like to know if the file isn't present.

(just to be clear, the bundle didn't try to included the minified version, it tried to include the full version, but the minified version was present in the script directory)

Do I have to write something custom to detect this or does MVC have anything built in to report this?

thanks

tony
  • 2,178
  • 2
  • 23
  • 40
  • Which link was not rendered on the client? The point of bundling is to reduce the number of files, so you only get one file per bundle on the client, unless you enable the diagnostics mode with `BundleTable.EnableOptimizations = false;`. With that setting on you can view the requests with Fiddler2 and see any missing scripts. – iCollect.it Ltd May 22 '14 at 09:34
  • Try this http://stackoverflow.com/questions/20869907/. The point is that you should have not minified version. At least the one, without the `.min.js`. As far as I know, the bundling runtime **never** uses the min, it always does minifing it itslef – Radim Köhler May 22 '14 at 09:36
  • Radim, thanks but that's not what the link says, "... will be searched, and if not found, the current will be minified", suggesting that if it is there it will be used – tony May 22 '14 at 09:44
  • 1
    With 'BundleTable.EnableOptimizations = false' I see the links for the existing files but, as an example, I have Add("~/Script/not_there.js") and nothing is rendered on the client. I want it to highlight that the file isn't there – tony May 22 '14 at 09:51
  • Incidently I could fix this in debug mode by replacing "~/Scripts/etc" with PreCheck("~/Scripts/etc") where PreCheck makes sure the files exists first time it runs, but I can't help but think someone has already done this – tony May 22 '14 at 10:02

4 Answers4

31

I came up to using the following extension methods for Bundle:

public static class BundleHelper
{
    [Conditional("DEBUG")] // remove this attribute to validate bundles in production too
    private static void CheckExistence(string virtualPath)
    {
        int i = virtualPath.LastIndexOf('/');
        string path = HostingEnvironment.MapPath(virtualPath.Substring(0, i));
        string fileName = virtualPath.Substring(i + 1);

        bool found = Directory.Exists(path);

        if (found)
        {
            if (fileName.Contains("{version}"))
            {
                var re = new Regex(fileName.Replace(".", @"\.").Replace("{version}", @"(\d+(?:\.\d+){1,3})"));
                fileName = fileName.Replace("{version}", "*");
                found = Directory.EnumerateFiles(path, fileName).FirstOrDefault(file => re.IsMatch(file)) != null;
            }
            else // fileName may contain '*'
                found = Directory.EnumerateFiles(path, fileName).FirstOrDefault() != null;
        }

        if (!found)
            throw new ApplicationException(String.Format("Bundle resource '{0}' not found", virtualPath));
    }

    public static Bundle IncludeExisting(this Bundle bundle, params string[] virtualPaths)
    {
        foreach (string virtualPath in virtualPaths)
            CheckExistence(virtualPath);

        return bundle.Include(virtualPaths);
    }

    public static Bundle IncludeExisting(this Bundle bundle, string virtualPath, params IItemTransform[] transforms)
    {
        CheckExistence(virtualPath);
        return bundle.Include(virtualPath, transforms);
    }
}

This way you don't have to call your helper method PreCheck() explicitly. It also supports ASP.NET's wildcards {version} and *:

bundles.Add(new ScriptBundle("~/test")
    .IncludeExisting("~/Scripts/jquery/jquery-{version}.js")
    .IncludeExisting("~/Scripts/lib*")
    .IncludeExisting("~/Scripts/model.js")
    );
Herman Kan
  • 2,253
  • 1
  • 25
  • 32
  • Useful stuff! One remark: you can replace `Where(file => re.IsMatch(file)).FirstOrDefault()` with `FirstOrDefault(file => re.IsMatch(file))` – user1068352 Feb 10 '16 at 14:14
  • 1
    And just in case it isn't obvious, this class should be inside a `namespace System.Web.Optimization{ }` block, and have the following usings: `using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Web.Hosting;` – Caius Jard Dec 28 '18 at 09:26
2

I can't believe this "anti-pattern" exists! If you see no errors, there are no errors!

Anyway, I like the solution above. Another would be to output the link / script even if it is missing when BundleTable.EnableOptimizations is false -- this is a very obvious thing to try when debugging, and then it will be obvious in the various inspectors / debuggers on browsers which file is missing. This seemed like such an obvious thing for debugging thing that I spent hours without realizing there were missing files. The other way, silently ignoring missing parts of the bundle, is so wrong that it reinforced my horrible debugging session.

Well, this won't bite me twice -- trauma endures.

Gerard ONeill
  • 3,914
  • 39
  • 25
1

Another way using BundleTable.VirtualPathProvider wrapper:

public class VirtualPathProviderExt : VirtualPathProvider
{
    private readonly VirtualPathProvider _provider;

    public VirtualPathProviderExt(VirtualPathProvider provider)
    {
        _provider = provider;
    }

    public override string CombineVirtualPaths(string basePath, string relativePath)
    {
        return _provider.CombineVirtualPaths(basePath, relativePath);
    }

    public override ObjRef CreateObjRef(Type requestedType)
    {
        return _provider.CreateObjRef(requestedType);
    }

    public override bool DirectoryExists(string virtualDir)
    {
        return _provider.DirectoryExists(virtualDir);
    }

    public override bool Equals(object obj)
    {
        return _provider.Equals(obj);
    }

    private static readonly Regex _ignorePathsRegex = new Regex(@"\.debug\.\w+$|^~/bundle.config$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    public override bool FileExists(string virtualPath)
    {
        var result = _provider.FileExists(virtualPath);
        if (!result && !_ignorePathsRegex.IsMatch(virtualPath))
        {
            Logger.Instance.Log(RecType.Error, "Bundle file not found: " + virtualPath);
        }

        return result;
    }

    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        return _provider.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }

    public override string GetCacheKey(string virtualPath)
    {
        return _provider.GetCacheKey(virtualPath);
    }

    public override VirtualDirectory GetDirectory(string virtualDir)
    {
        return _provider.GetDirectory(virtualDir);
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        return _provider.GetFile(virtualPath);
    }

    public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
    {
        return _provider.GetFileHash(virtualPath, virtualPathDependencies);
    }

    public override int GetHashCode()
    {
        return _provider.GetHashCode();
    }

    public override object InitializeLifetimeService()
    {
        return _provider.InitializeLifetimeService();
    }

    public override string ToString()
    {
        return _provider.ToString();
    }
}

Bundle helper:

public static class BundleHelpers
{
    public static void InitBundles()
    {
        if (!(BundleTable.VirtualPathProvider is VirtualPathProviderExt))
        {
            BundleTable.VirtualPathProvider = new VirtualPathProviderExt(BundleTable.VirtualPathProvider);
        }
    }
}

And run BundleHelpers.InitBundles() in BundleConfig.cs:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        BundleHelpers.InitBundles();
        ...
Nikolay Makhonin
  • 1,087
  • 12
  • 18
0

I changed the code a bit. Instead of throwing an error it will not add any bundle file. this is required if you are using same common bundle config for multiple projects

public static class BundleHelper
{
    private static bool CheckExistence(string virtualPath)
    {
        int i = virtualPath.LastIndexOf('/');
        string path = HostingEnvironment.MapPath(virtualPath.Substring(0, i));
        string fileName = virtualPath.Substring(i + 1);

        bool found = Directory.Exists(path);

        if (found)
        {
            if (fileName.Contains("{version}"))
            {
                var re = new Regex(fileName.Replace(".", @"\.").Replace("{version}", @"(\d+(?:\.\d+){1,3})"));
                fileName = fileName.Replace("{version}", "*");
                found = Directory.EnumerateFiles(path, fileName).Where(file => re.IsMatch(file)).FirstOrDefault() != null;
            }
            else // fileName may contain '*'
                found = Directory.EnumerateFiles(path, fileName).FirstOrDefault() != null;
        }
        return found;
        //if (!found)
        //throw new ApplicationException(String.Format("Bundle resource '{0}' not found", virtualPath));
    }

    public static Bundle IncludeExisting(this Bundle bundle, params string[] virtualPaths)
    {
        foreach (string virtualPath in virtualPaths)
            if (CheckExistence(virtualPath))
            {
                bundle.Include(virtualPath);
            }

        return bundle;
    }

    public static Bundle IncludeExisting(this Bundle bundle, string virtualPath, params IItemTransform[] transforms)
    {
        if (CheckExistence(virtualPath))
            bundle.Include(virtualPath, transforms);
        return bundle;
    }
}
Crennotech
  • 521
  • 5
  • 8