18

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

5
  • 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. Commented May 22, 2014 at 9:34
  • Try this 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 Commented May 22, 2014 at 9: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 Commented May 22, 2014 at 9: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 Commented May 22, 2014 at 9: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 Commented May 22, 2014 at 10:02

4 Answers 4

32

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")
    );
Sign up to request clarification or add additional context in comments.

2 Comments

Useful stuff! One remark: you can replace Where(file => re.IsMatch(file)).FirstOrDefault() with FirstOrDefault(file => re.IsMatch(file))
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;
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.

Comments

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();
        ...

Comments

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;
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.