/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.assets; import java.util.List; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Router; import org.jooby.handlers.AssetHandler; import org.jooby.internal.assets.AssetHandlerWithCompiler; import org.jooby.internal.assets.AssetVars; import org.jooby.internal.assets.LiveCompiler; import com.google.common.collect.ImmutableList; import com.google.inject.Binder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; /** * <h1>assets</h1> * <p> * The asset module is library to concatenate, minify or compress JavaScript and CSS * assets. It also adds the ability to write these assets in other languages and process/compile * them to another language. Finally, it help you to write high quality code by validate JavaScript * and CSS too. * </p> * <p> * A variety of processors are available (jshint, csslint, jscs, uglify, closure-compiler, etc..), * but * also you might want to write your owns. * </p> * * <h2>getting started</h2> * <p> * The first thing you need to do is to define your assets. Definition is done in your * <code>.conf</code> file or in a special file: <code>assets.conf</code>. * </p> * * <strong>assets.conf</strong> * <pre> * assets { * fileset { * home: [assets/home.js, assets/home.css] * } * } * </pre> * * <strong>App.java</strong> * <pre> * { * use(new Assets()); * } * </pre> * * <p> * The assets module will publish 4 request local variables for <code>home</code> fileset: * <code>_css</code> and <code>_js</code> each of these variables is a list of string with the * corresponding files. There are two more variables: <code>_styles</code> and <code>_scripts</code> * : * </p> * * <pre> * <html> * <head> * {{{home_styles}}} * <body> * ... * {{{home_scripts}} * </body> * </head> * </html> * </pre> * * <p> * The variables: <code>_styles</code> and <code>_scripts</code> produces one ore more * <code>link</code> and <code>script</code> tags. The example above, shows you how to * render these variables in the template engine of your choice (handlebars, here). * </p> * * <p> * Now, let's see how to configure the Maven plugin to process our assets at build-time: * </p> * * <strong>pom.xml</strong> * * <pre> * <plugin> * <groupId>org.jooby</groupId> * <artifactId>jooby-maven-plugin</artifactId> * <executions> * <execution> * <goals> * <goal>assets</goal> * </goals> * </execution> * </executions> * </plugin> * </pre> * * <p> * The plugin will process all your assets and include them to the final <code>.jar</code>, * <code>.zip</code> or <code>.war</code>. * </p> * * <p> * Cool, isn't? * </p> * * <h2>how it works?</h2> * <p> * The <code>assets.fileset</code> defines all your assets. In <code>dev</code> assets are * rendered/processed at runtime. In <code>prod</code> at built-time. * </p> * <p> * Assets are rendered at runtime using <code>*_styles</code> or <code>*_scripts * </code> variables. So you define your assets in one single place: <code>assets.conf</code>. * </p> * <p> * Also, at build-time, the asset compiler concatenates all the files from a fileset and * generate a fingerprint. The fingerprint is a SHA-1 hash of the content of the fileset. Thanks to * the fingerprint an asset can be cached it for ever! Defaults cache max age is: * <code>365 days</code>. * </p> * <p> * That isn't all! the <code>*_styles</code> and <code>*_scripts</code> are updated with the * fingerprint version of assets, so you don't have to do or change anything in your views! It just * works!!! * </p> * * <h2>fileset</h2> * <p> * A fileset is a group of assets within a name. The fileset name is expanded into 4 request local * variables, for example: * </p> * <pre> * assets { * fileset { * home: [assets/home.js, assets/home.css] * pageA: [assets/pageA.js, assets/pageA.css] * } * } * </pre> * * <p> * Produces 4 variables for <code>home</code>: * </p> * * <ul> * <li>home_css: a list of all the <code>css</code> files</li> * <li>home_styles: a string, with all the <code>css</code> files rendered as <code>link</code> tags * </li> * <li>home_js: a list of all the <code>js</code> files</li> * <li>home_scripts: a string, with all the <code>js</code> files rendered as <code>script</code> * tags</li> * </ul> * * <p> * Another 4 variables will be available for the <code>pageA</code> fileset! * </p> * * <h3>extending filesets</h3> * <p> * Extension or re-use of filesets is possible via the: <code><</code> operator: * </p> * <pre> * assets { * fileset { * base: [js/lib/jquery.js, css/normalize.css] * home < base: [js/home.js] * pageA < base: [js/pageA.js] * } * } * </pre> * * <h2>processors</h2> * <p> * An {@link AssetProcessor} usually checks or modify an asset content in one way or another. They * are defined in the <code>assets.conf</code> files using the <code>pipeline</code> construction: * </p> * * <pre> * assets { * fileset { * home: [js/home.js, css/home.css] * } * * pipeline { * dev: [jshint, jscs, csslint, sass] * dist: [uglify, sass, clean-css] * } * } * </pre> * * <p> * Example above, defines a <strong>pipeline</strong> for development (dev) and one generic for prod * (dist). * </p> * <p> * In <code>dev</code> the code will be checked it against js-hint, jscs and csslint! But * also, we want to use sass for css!! * </p> * <p> * The generic <code>dist</code> will be used it for any other environment and here we just want to * optimize our javascript code with uglify, compile sass to css and then optimize the css using * clean-css!! * </p> * * <p> * For more information about processor, have a look at the {@link AssetProcessor} doc. * </p> * * <h2>aggregators</h2> * <p> * Contributes new or dynamically generated content to a <code>fileset</code>. Content generated by * an aggregator might be processed by an {@link AssetProcessor}. * </p> * * <h3>how to use it?</h3> * <p> * First thing to do is to add the dependency: * </p> * <pre> * <dependency> * <groupId>org.jooby</groupId> * <artifactId>jooby-assets-dr-svg-sprites</artifactId> * <scope>provided</scope> * </dependency> * </pre> * * <p> * Did you see the <strong>provided</strong> scope? We just need the aggregator for development, * because assets are processed at runtime. For <code>prod</code>, assets are processed at * built-time via Maven/Gradle plugin, so we don't need it. This also, helps to keep our * dependencies and the jar size small. * </p> * * <p> * Now we have the dependency all we have to do is to add the <code>svg-sprites</code> aggregator to * a fileset: * </p> * * <pre> * assets { * fileset { * home: [ * // 1) Add the aggregator to a fileset * svg-sprites, * css/style.css, * js/app.js * ] * } * * svg-sprites { * // 2) The `css/sprite.css` file is part of the `home` fileset. * spritePath: "css/sprite.css" * * spriteElementPath: "images/svg", * } * } * </pre> * * <p> * Here for example, the <code>svg-sprites</code> aggregator contributes the * <code>css/sprite.css</code> file to the <code>home</code> fileset. The fileset then looks like: * </p> * * <pre> * assets { * fileset { * home: [ * css/sprite.css, * css/style.css, * js/app.js * ] * } * } * </pre> * <p> * Replaces the aggregator name with one or more files from {@link AssetAggregator#fileset()} * method. * </p> * * @author edgar * @since 0.11.0 */ public class Assets implements Jooby.Module { @Override public void configure(final Env env, final Config config, final Binder binder) throws Throwable { String envname = env.name(); boolean dev = "dev".equals(envname); ClassLoader loader = getClass().getClassLoader(); Config conf = conf(dev, loader, config); String cpath = config.getString("application.path"); AssetCompiler compiler = new AssetCompiler(loader, conf); Router routes = env.router(); List<String> dist = dev ? ImmutableList.of("dev") : ImmutableList.of(envname, "dist"); String prefix = dist.stream() .filter(it -> conf.hasPath("assets." + it + ".prefix")) .findFirst() .map(it -> conf.getString("assets." + it + ".prefix")) .orElse(cpath); routes.use("*", "*", new AssetVars(compiler, prefix, !dev)).name("/assets/vars"); // live compiler? boolean watch = conf.hasPath("assets.watch") ? conf.getBoolean("assets.watch") : dev; if (watch) { LiveCompiler liveCompiler = new LiveCompiler(conf, compiler); env.onStart(liveCompiler::start); env.onStop(liveCompiler::stop); routes.use("*", "*", liveCompiler).name("/assets/compiler"); } AssetHandler handler = dev ? new AssetHandlerWithCompiler("/", compiler) : new AssetHandler("/"); handler.etag(conf.getBoolean("assets.etag")) .cdn(conf.getString("assets.cdn")) .lastModified(conf.getBoolean("assets.lastModified")); handler.maxAge(conf.getString("assets.cache.maxAge")); compiler.patterns().forEach(pattern -> routes.get(pattern, handler)); } private Config conf(final boolean dev, final ClassLoader loader, final Config conf) { final Config[] confs; if (!dev) { confs = new Config[]{ ConfigFactory.parseResources(loader, "assets." + conf.getString("application.env").toLowerCase() + ".conf"), ConfigFactory.parseResources(loader, "assets.dist.conf"), ConfigFactory.parseResources(loader, "assets.conf") }; for (Config it : confs) { if (!it.isEmpty()) { return it.withFallback(conf).resolve(); } } } return ConfigFactory.parseResources(loader, "assets.conf").withFallback(conf).resolve(); } }