I\'m trying to use MVC4 bundling to group some of my less files, but it looks like the import path I\'m using is off. My directory structure is:
static/
Following on from RockResolve below, to use the MicrosoftAjax minifier, reference it as the default CSS minifier in web.config as opposed to passing it in as an argument.
From https://bundletransformer.codeplex.com/wikipage/?title=Bundle%20Transformer%201.7.0%20Beta%201#BundleTransformerMicrosoftAjax_Chapter
To make MicrosoftAjaxCssMinifier the default CSS-minifier and MicrosoftAjaxJsMinifier the default JS-minifier, you need to make changes to the Web.config file. In defaultMinifier attribute of \configuration\bundleTransformer\core\css element must be set value equal to MicrosoftAjaxCssMinifier, and in same attribute of \configuration\bundleTransformer\core\js element - MicrosoftAjaxJsMinifier.
I know that this "should be a comment to Ben Cull's post", but it adds a little extra that would be impossible to add in a comment. So vote me down if you must. Or close me.
Ben's blog post does it all, except it doesn't specify minification.
So install the BundleTransformer.Less package as Ben suggests and then, if you want minification of your css, do the following (in ~/App_Start/BundleConfig.cs):
var cssTransformer = new CssTransformer();
var jsTransformer = new JsTransformer();
var nullOrderer = new NullOrderer();
var css = new Bundle("~/bundles/css")
.Include("~/Content/site.less");
css.Transforms.Add(cssTransformer);
css.Transforms.Add(new CssMinify());
css.Orderer = nullOrderer;
bundles.Add(css);
The added line is:
css.Transforms.Add(new CssMinify());
Where CssMinify
is in System.Web.Optimizations
I am so relieved to get around the @import issue and the resulting file with .less extension not found that I don't care who votes me down.
If, on the contrary, you feel the urge to vote for this answer, please give your vote to Ben.
So there.
I have been through the same problem, seeing the same error message. Looking for a solution on the internet brought me here. My problem was as follows:
Within a less file I had at some point an incorrect style which was giving me a warning. The less file couldn't be parsed. I got rid of the error message by removing the incorrect line.
I hope this helps someone.
There is code posted on GitHub Gist that works well with @import and dotLess: https://gist.github.com/2002958
I tested it with Twitter Bootstrap and it works well.
ImportedFilePathResolver.cs
public class ImportedFilePathResolver : IPathResolver
{
private string currentFileDirectory;
private string currentFilePath;
/// <summary>
/// Initializes a new instance of the <see cref="ImportedFilePathResolver"/> class.
/// </summary>
/// <param name="currentFilePath">The path to the currently processed file.</param>
public ImportedFilePathResolver(string currentFilePath)
{
CurrentFilePath = currentFilePath;
}
/// <summary>
/// Gets or sets the path to the currently processed file.
/// </summary>
public string CurrentFilePath
{
get { return currentFilePath; }
set
{
currentFilePath = value;
currentFileDirectory = Path.GetDirectoryName(value);
}
}
/// <summary>
/// Returns the absolute path for the specified improted file path.
/// </summary>
/// <param name="filePath">The imported file path.</param>
public string GetFullPath(string filePath)
{
filePath = filePath.Replace('\\', '/').Trim();
if(filePath.StartsWith("~"))
{
filePath = VirtualPathUtility.ToAbsolute(filePath);
}
if(filePath.StartsWith("/"))
{
filePath = HostingEnvironment.MapPath(filePath);
}
else if(!Path.IsPathRooted(filePath))
{
filePath = Path.Combine(currentFileDirectory, filePath);
}
return filePath;
}
}
LessMinify.cs
public class LessMinify : IBundleTransform
{
/// <summary>
/// Processes the specified bundle of LESS files.
/// </summary>
/// <param name="bundle">The LESS bundle.</param>
public void Process(BundleContext context, BundleResponse bundle)
{
if(bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
foreach(FileInfo file in bundle.Files)
{
SetCurrentFilePath(lessParser, file.FullName);
string source = File.ReadAllText(file.FullName);
content.Append(lessEngine.TransformToCss(source, file.FullName));
content.AppendLine();
AddFileDependencies(lessParser);
}
bundle.Content = content.ToString();
bundle.ContentType = "text/css";
//base.Process(context, bundle);
}
/// <summary>
/// Creates an instance of LESS engine.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, false);
}
/// <summary>
/// Adds imported files to the collection of files on which the current response is dependent.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private void AddFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach(string importedFilePath in lessParser.Importer.Imports)
{
string fullPath = pathResolver.GetFullPath(importedFilePath);
HttpContext.Current.Response.AddFileDependency(fullPath);
}
lessParser.Importer.Imports.Clear();
}
/// <summary>
/// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
/// </summary>
/// <param name="lessParser">The LESS prser.</param>
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
if(importer != null)
{
var fileReader = importer.FileReader as FileReader;
if(fileReader != null)
{
return fileReader.PathResolver;
}
}
return null;
}
/// <summary>
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using custom <see cref="IPathResolver"/> implementation.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <param name="currentFilePath">The path to the currently processed file.</param>
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if(importer != null)
{
var fileReader = importer.FileReader as FileReader;
if(fileReader == null)
{
importer.FileReader = fileReader = new FileReader();
}
var pathResolver = fileReader.PathResolver as ImportedFilePathResolver;
if(pathResolver != null)
{
pathResolver.CurrentFilePath = currentFilePath;
}
else
{
fileReader.PathResolver = new ImportedFilePathResolver(currentFilePath);
}
}
else
{
throw new InvalidOperationException("Unexpected importer type on dotless parser");
}
}
}
Here's what I did:
Added Twitter Bootstrap Nuget module.
Added this to my _Layout.cshtml file:
<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/twitterbootstrap/less")" rel="stylesheet" type="text/css" />
Note that I renamed my "less" folder to twitterbootstrap just to demonstrate that I could
Moved all the less files into a subfolder called "imports" except bootstrap.less and (for responsive design) responsive.less.
~/Content/twitterbootstrap/imports
Added a configuration in the web.config:
<add key="TwitterBootstrapLessImportsFolder" value="imports" />
Created two classes (slight modification of the class above):
using System.Configuration;
using System.IO;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.configuration;
using dotless.Core.Input;
namespace TwitterBootstrapLessMinify
{
public class TwitterBootstrapLessMinify : CssMinify
{
public static string BundlePath { get; private set; }
public override void Process(BundleContext context, BundleResponse response)
{
setBasePath(context);
var config = new DotlessConfiguration(dotless.Core.configuration.DotlessConfiguration.GetDefault());
config.LessSource = typeof(TwitterBootstrapLessMinifyBundleFileReader);
response.Content = Less.Parse(response.Content, config);
base.Process(context, response);
}
private void setBasePath(BundleContext context)
{
var importsFolder = ConfigurationManager.AppSettings["TwitterBootstrapLessImportsFolder"] ?? "imports";
var path = context.BundleVirtualPath;
path = path.Remove(path.LastIndexOf("/") + 1);
BundlePath = context.HttpContext.Server.MapPath(path + importsFolder + "/");
}
}
public class TwitterBootstrapLessMinifyBundleFileReader : IFileReader
{
public IPathResolver PathResolver { get; set; }
private string basePath;
public TwitterBootstrapLessMinifyBundleFileReader() : this(new RelativePathResolver())
{
}
public TwitterBootstrapLessMinifyBundleFileReader(IPathResolver pathResolver)
{
PathResolver = pathResolver;
basePath = TwitterBootstrapLessMinify.BundlePath;
}
public bool DoesFileExist(string fileName)
{
fileName = PathResolver.GetFullPath(basePath + fileName);
return File.Exists(fileName);
}
public string GetFileContents(string fileName)
{
fileName = PathResolver.GetFullPath(basePath + fileName);
return File.ReadAllText(fileName);
}
}
}
My implementation of the IFileReader looks at the static member BundlePath of the TwitterBootstrapLessMinify class. This allows us to inject a base path for the imports to use. I would have liked to take a different approach (by providing an instance of my class, but I couldn't).
Finally, I added the following lines to the Global.asax:
BundleTable.Bundles.EnableDefaultBundles();
var lessFB = new DynamicFolderBundle("less", new TwitterBootstrapLessMinify(), "*.less", false);
BundleTable.Bundles.Add(lessFB);
This effectively solves the problem of the imports not knowing where to import from.
As of Feb 2013: Michael Baird's great solution was superceeded by the "BundleTransformer.Less Nuget Package" answer referred to in Ben Cull's post. Similar answer at: http://blog.cdeutsch.com/2012/08/using-less-and-twitter-bootstrap-in.html
Cdeutsch's blog & awrigley's post adding minification is good, but apparently not now the correct approach.
Someone else with the same solution got some answers from a BundleTransformer author: http://geekswithblogs.net/ToStringTheory/archive/2012/11/30/who-could-ask-for-more-with-less-css-part-2.aspx. See the comments at the bottom.
In summary use BundleTransformer.MicrosoftAjax instead of the built in built-in minifiers. e.g. css.Transforms.Add(new CssMinify()); replaced with css.Transforms.Add(new BundleTransformer.MicrosoftAjax());