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/
less/
mixins.less
admin/
user.less
In user.less, I'm attempting to import mixins.less using this:
@import "../mixins.less";
This used to work for me before when using chirpy with dotless, but now I noticed ELMAH was getting mad at me, saying this:
System.IO.FileNotFoundException:
You are importing a file ending in .less that cannot be found.
File name: '../mixins.less'
Am I supposed to use a different @import
with MVC4?
Here's the less class and global.asax.cs code I'm using to attempt this:
LessMinify.cs
...
public class LessMinify : CssMinify
{
public LessMinify() {}
public override void Process(BundleContext context, BundleResponse response)
{
response.Content = Less.Parse(response.Content);
base.Process(context, response);
}
}
...
Global.asax.cs
...
DynamicFolderBundle lessFB =
new DynamicFolderBundle("less", new LessMinify(), "*.less");
BundleTable.Bundles.Add(lessFB);
Bundle AdminLess = new Bundle("~/AdminLessBundle", new LessMinify());
...
AdminLess.AddFile("~/static/less/admin/user.less");
BundleTable.Bundles.Add(AdminLess);
...
I've written a quick blog post about Using LESS CSS With MVC4 Web Optimization.
It basically boils down to using the BundleTransformer.Less Nuget Package and changing up your BundleConfig.cs.
Tested with bootstrap.
EDIT: Should mention the reason I say this, is I also ran into the @import directory structure issue, and this library handles it correctly.
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");
}
}
}
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.
A work around that I found that was really helpful was to set the directory before running Less.Parse inside of the LessMinify.Process(). Here is how I did it:
public class LessTransform : IBundleTransform
{
private string _path;
public LessTransform(string path)
{
_path = path;
}
public void Process(BundleContext context, BundleResponse response)
{
Directory.SetCurrentDirectory(_path);
response.Content = Less.Parse(response.Content);
response.ContentType = "text/css";
}
}
Then passing in the path when creating the less transform object like so:
lessBundle.Transforms.Add(
new LessTransform(HttpRuntime.AppDomainAppPath + "/Content/Less")
);
Hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With