Compile .less files to .css in maven (again using GMaven)

I have recently come across LessCSS, a CSS meta-language that introduces code re-usability. I have been looking for a solution like this for some years now, as I hate having to write the same stuff over and over again. So I immediately created some .less files in a current web project of mine, but when it came to automatically compiling them, things turned out to be less easy.

LessCss was originally written in ruby, so my first approach was to embed jruby in maven and do the compiling like that. However, JRuby is huge and it takes ages to start up, and also there are issues about file paths to the jruby.jar that are not allowed to contain spaces etc. All in all it was just a pain.

Then I had the idea to do the processing at runtime, not compile time, using a PackageResource in wicket. While doing some research for this I came across this blog post, where the author had done exactly this. However, I still believe this kind of pre-processing should be done at compile time, so I was very happy to see that the author uses a slightly different approach using the re-written javascript version of LessCss embedded in Mozilla Rhino. After some hacking on my part this solution works perfectly for me using GMaven. Basically I rewrote the inner workings of the visural less processing in groovy, so my language stack is this:

Java (Maven) ==> Groovy (GMaven) ==> JavaScript (Rhino)

So here is the groovy script that does the work:

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ScriptableObject;
import org.slf4j.Logger;

public class LessCssJsCompiler {

    private final String lessjs =
        new File("src/main/javascript/less.js").text;
    private final ContextFactory cf;
    private final Context c;
    private final ScriptableObject so;
    private final Logger log

    private final String runjs = """\
    var lessIt = function(css) {
        var result;
        var parser = new less.Parser();

        parser.parse(css, function (e, root) {
            result = root.toCSS();
        });
        return result;
    };
""";

    private File srcRoot;
    private File trgRoot;

    public LessCssJsCompiler(File src, File trg, Logger log){
        this.log = log;
        this.srcRoot=src.getAbsoluteFile();
        log.info "Source folder: ${this.srcRoot}";
        this.trgRoot=trg.getAbsoluteFile();
        log.info "Target folder: ${this.trgRoot}";
        this.cf = new ContextFactory();
        this.c = cf.enterContext();
        this.so = c.initStandardObjects();
        c.setOptimizationLevel(9);
        c.evaluateString(so, lessjs, "less.js", 1, null);
        c.evaluateString(so, runjs, "run.js", 1, null);
        processFolder(this.srcRoot);
    }

    void processFolder(File folder){
        folder.eachFile {
            if(it.isDirectory()){
                if(it.name != 'CVS'){ // or whatever your SCM is
                    processFolder it;
                }
            }else{
                if(it.name.endsWith('.less'))
                    processFile(it, getMirrorFile(it));
            }
        }
    }

    void processFile (File input, File output){
        if(output.exists() && output.lastModified() > input.lastModified()){
            log.info "File $output is up to date, skipping."
        }else{
            log.info "Compiling file $input to $output";
            output.parentFile.mkdirs();
            output.text = less(input.text);
        }
    }

    File getMirrorFile(File input){
        File parentFolder = new File(input.parentFile.absolutePath.
                replace(srcRoot.absolutePath, trgRoot.absolutePath));
        def output = new File(
                parentFolder,
                input.name.replace(".less", ".css")
                );
        log.info "Using output file $output for input file $input";
        return output;
    }

    public String less(String data) {
        String lessitjs = "lessIt(\"" +
               data.replace("\"", "\\\"")
                   .replaceAll( /\s+/, ' ' ) // strip line feeds
                   .replaceAll( /\/\*.*?\*\//, '' )+"\");"; //strip comments
        return this.c.evaluateString(
                         so, lessitjs, "lessitjs.js", 1, null).toString();
    }
}

The relevant part of the POM.xml is this:

inside dependencies:

<dependency>
    <groupId>rhino</groupId>
    <artifactId>js</artifactId>
    <version>1.7R2</version>
    <scope>provided</scope>
</dependency>

(you could also use a plugin dependency, but when I use a project dependency, eclipse will give me autocompletion etc)

inside build/plugins:

<plugin>
 <groupId>org.codehaus.groovy.maven</groupId>
 <artifactId>gmaven-plugin</artifactId>
 <version>1.0</version>
 <executions>
 <execution>
 <id>translate-css</id>
 <phase>process-resources</phase>
 <goals>
 <goal>execute</goal>
 </goals>
 <configuration>
 <scriptpath>
 <element>${pom.basedir}/src/main/groovy</element>
 </scriptpath>
 <source>
 <![CDATA[

 new LessCssJsCompiler(
     new File(pom.basedir, "src/main/java"),
     new File(pom.build.outputDirectory),
     log
 );
 ]]>
 </source>
 </configuration>
 </execution>
 </executions>
 </plugin>

In order for this to work, the less.js script (I am using this version) must be inside src/main/javascript and the groovy script must be inside src/main/groovy.

Also, you probably don’t want the .less files in your target folder, so you should remove them from resource processing

for src/main/resources:

<resource>
     <directory>src/main/resources</directory>
     <excludes>
         <exclude>**/*.less</exclude>
     </excludes>
</resource>

or for src/main/java (which is probably where stuff is if you use wicket):

<resource>
     <directory>src/main/java</directory>
     <excludes>
         <exclude>**/*.java</exclude>
         <exclude>**/*.less</exclude>
     </excludes>
</resource>
About these ads
This entry was posted in CSS, Groovy, JavaScript, Maven. Bookmark the permalink.

10 Responses to Compile .less files to .css in maven (again using GMaven)

  1. Mace says:

    I really like your solution!!!
    I only got one question. At the moment i can’t get the import statements to work. Do you have solution for that? or what is the way you are using the import statements.

    thanks in advance.

    greetz mace

  2. mostlymagic says:

    mace, what you see is the exact file contents of the groovy script.

    If you use eclipse, it will sometimes complain about the imports, but maven should be able to run this as is.

  3. Mace says:

    You’re right if I build the less files with maven it works.
    Thanks

    Mace

  4. Alistair says:

    Getting an error when I run this:

    groovy.lang.GroovyRuntimeException: Could not find matching constructor for: LessCssJsCompiler(java.io.File, java.io.File, org.codehaus.groovy.maven.gossip.Gossip$LoggerImpl)

    src: https://github.com/AlistairB/personalwebsite

    Any ideas?

  5. Alistair says:

    Worked. You rock!

    Thanks so much!

  6. Unfortunatelly when I use:
    @import “otherfile.less”;

    I got error:
    org.mozilla.javascript.EcmaError: TypeError: Cannot call property importer in object

  7. mark says:

    Very nice and hugely useful.

    I’ll probably figure this out after I click ‘Post’, but after a couple hours of staring at this, I’m stumped. I’m porting this code to a simple Java maven project, and am running into this stacktrace

    Exception in thread “main” org.mozilla.javascript.EcmaError: ReferenceError: “less” is not defined. (run.js#1)
    at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3785)
    at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3763)
    at org.mozilla.javascript.ScriptRuntime.notFoundError(ScriptRuntime.java:3848)
    at org.mozilla.javascript.ScriptRuntime.nameOrFunction(ScriptRuntime.java:1847)
    at org.mozilla.javascript.ScriptRuntime.name(ScriptRuntime.java:1786)
    at org.mozilla.javascript.gen.run_js_2._c_anonymous_1(run.js:1)
    at org.mozilla.javascript.gen.run_js_2.call(run.js)
    at org.mozilla.javascript.optimizer.OptRuntime.callName(OptRuntime.java:97)
    at org.mozilla.javascript.gen.lessitjs_js_3._c_script_0(lessitjs.js:1)
    at org.mozilla.javascript.gen.lessitjs_js_3.call(lessitjs.js)
    at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:426)
    at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3178)
    at org.mozilla.javascript.gen.lessitjs_js_3.call(lessitjs.js)
    at org.mozilla.javascript.gen.lessitjs_js_3.exec(lessitjs.js)
    at org.mozilla.javascript.Context.evaluateString(Context.java:1111)
    at org.petrovic.lesscss.LessCompiler.compile(LessCompiler.java:55)
    at org.petrovic.lesscss.LessCompiler.main(LessCompiler.java:22)

    despite my belief that my code should behave the same as yours. Here’s what I have:

    public LessCompiler() {
    String lessSrc = “less-1.6.1.js”;
    String runSrc = “run.js”;

    final InputStream lessIs = getClass().getClassLoader().getResourceAsStream(lessSrc);
    final InputStream runIs = getClass().getClassLoader().getResourceAsStream(runSrc);
    try {
    final ContextFactory contextFactory = new ContextFactory();
    context = contextFactory.enterContext();
    scriptableObject = context.initStandardObjects();
    context.setOptimizationLevel(9);
    context.evaluateString(scriptableObject, asString(lessIs), lessSrc, 1, null);
    context.evaluateString(scriptableObject, asString(runIs), runSrc, 1, null);
    lessIs.close();
    runIs.close();
    } catch (IOException ex) {
    throw new IllegalStateException(“Failed reading javascript less.js”, ex);
    }
    }

    public String compile(InputStream input) throws IOException {
    String data = asString(input);
    final String replace = data.replace(“\””, “\\\””).replaceAll(“\n”, “”).replaceAll(“\r”, “”);
    StringBuilder sb = new StringBuilder(“lessIt(\””).append(replace).append(“\”)”);
    String lessitjs = sb.toString();
    String result = context.evaluateString(scriptableObject, lessitjs, “lessitjs.js”, 1, null).toString();
    return result;
    }

    My problem reeks needing a one-line fix, but for the life of me, I can’t see which line.

    Would someone be kind enough to point me in the right direction.

    Thanks much.

  8. mostlymagic says:

    Mark, I’d recommend to look at the wro4j maven plugin http://code.google.com/p/wro4j/wiki/MavenPlugin It should be an easier solution and it includes Less support.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s