by @tednaleid
<link rel="stylesheet" href="/test-app/assets/main.css" /><link rel="stylesheet" href="/test-app/assets/mobile.css" /><link rel="stylesheet" href="/test-app/assets/app.css" /><script src="/test-app/assets/vendor/angular/angular-route.js" type="text/javascript" ></script><script src="/test-app/assets/vendor/jquery/jquery.js" type="text/javascript" ></script><script src="/test-app/assets/vendor/mongolab/mongolab-resource.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/admin.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/projects/admin-projects.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/users/admin-users-edit.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/users/admin-users-list.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/users/admin-users.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/users/uniqueEmail.js" type="text/javascript" ></script><script src="/test-app/assets/app/admin/users/validateEquals.js" type="text/javascript" ></script><script src="/test-app/assets/app/app.js" type="text/javascript" ></script><script src="/test-app/assets/app/dashboard/dashboard.js" type="text/javascript" ></script><script src="/test-app/assets/app/projects/productbacklog/productbacklog.js" type="text/javascript" ></script><script src="/test-app/assets/app/projects/projects.js" type="text/javascript" ></script><script src="/test-app/assets/app/projects/sprints/sprints.js" type="text/javascript" ></script><script src="/test-app/assets/app/projects/sprints/tasks/tasks.js" type="text/javascript" ></script><script src="/test-app/assets/app/projectsinfo/projectsinfo.js" type="text/javascript" ></script><script src="/test-app/assets/app.js" type="text/javascript" ></script>
<link rel="stylesheet" href="/test-app/assets/app-08f92c4662379e3e9a56541af70c871c.css"/><script src="/test-app/assets/app-e1ec7cb3d4c455fac6e62f1faa6232f9.js" type="text/javascript" ></script>
grails-app/assets
//= encoding UTF-8//= require lib/angular.js//= require_self//= require_tree model(function () {'use strict';console.log("loaded app.js");…});})();
assets/javascripts/app.js
/**= encoding UTF-8*= require fonts*= require bootstrap*= require_self*/input.ng-invalid.ng-dirty:not(:focus) {border-color: #d43232;background-color: #f2dede;}…
assets/stylesheets/app.css
<head><title><g:layoutTitle default="Some Title" /></title>…<asset:link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/><asset:stylesheet href="app.css"/><g:layoutHead /></head><body><asset:image src="logo.png" width="200" height="200"/>…<g:layoutBody />…<asset:javascript src="app.js"/><asset:deferredScripts/></body>
grails-app/views/layouts/main.gsp
<div><h1>foobar</h1><asset:script type="text/javascript">console.log("on foobar page");</asset:script></div>
grails-app/views/user/index.gsp
emitted at bottom of layout at <asset:deferredScripts/>
grails run-app// from: <asset:stylesheet href="app.css"/><link rel="stylesheet" href="/myapp/assets/fonts.css" /><link rel="stylesheet" href="/myapp/assets/bootstrap.css" /><link rel="stylesheet" href="/myapp/assets/app.css" />
// from: <asset:javascript src="app.js"/><script src="/myapp/assets/lib/angular.js" type="text/javascript" ></script><script src="/myapp/assets/app.js" type="text/javascript" ></script><script src="/myapp/assets/model/Foo.js" type="text/javascript" ></script><script src="/myapp/assets/model/Bar.js" type="text/javascript" ></script>
// HTTP 1.1Cache-Control:no-cache, no-store, must-revalidate// HTTP 1.0Pragma:no-cache// for proxiesExpires:Thu, 01 Jan 1970 00:00:00 GMT
grails war or grails run-warassetClean and assetCompile eventsassets Folder…assets/logo-7b9776076d5fceef4993b55c9383dedd.jpgassets/logo.jpg…assets/app-2b367068131a9dd4f31c18f635eb7e6c.cssassets/app-2b367068131a9dd4f31c18f635eb7e6c.css.gzassets/app.cssassets/app.css.gz…assets/app-4bf9739e18a6d5f665c42ecb7edbd339.jsassets/app-4bf9739e18a6d5f665c42ecb7edbd339.js.gzassets/app.jsassets/app.js.gz…assets/manifest.properties…
manifest.properties File With ETag Mapping For Each Asset…logo.jpg=logo-7b9776076d5fceef4993b55c9383dedd.jpg…app.css=app-2b367068131a9dd4f31c18f635eb7e6c.cssbootstrap.css=bootstrap-2b367068131a9dd4f31c18f635eb7e6c.cssfonts.css=fonts-81f3e38a6e878acd57e3d51912c37b04.css…lib/angular.js=lib/angular-de39960f9b36bbbd76e76e7ed0086922.jsapp.js=app-4bf9739e18a6d5f665c42ecb7edbd339.js…
AssetPipelineFilter Processes /assets/* Requests/assetsgrails.assets.url = "https://cdn.example.com/"// orgrails.assets.url = { request ->if(request.isSecure()) {return "https://cdn.example.com/"} else {return "http://cdn.example.com/"}}
(in Config.groovy)
automate uploading with the cdn-asset-pipeline plugin
AssetFilepackage asset.pipelineimport asset.pipeline.AbstractAssetFileclass MyAssetFile extends AbstractAssetFile { // implements AssetFilestatic final String contentType = 'application/javascript'static extensions = ['js-myfile'] // MUST BE UNIQUE ACROSS ALL `AssetFile`sstatic final String compiledExtension = 'js'static processors = [MyFileProcessor]String directiveForLine(String line) {// identifies the directive in manifest lines at top of file// i.e. those starting with '//='line.find(/\/\/=(.*)/) { fullMatch, directive -> directive }}}
package asset.pipelineclass MyFileProcessor implements Processor {MyFileProcessor(AssetCompiler compiler) {// compiler gives us access to the options/rules/paths// that asset-pipeline is running withsuper(compiler)}// inputText - a String holding the contents of the asset// assetFile - an AssetFile instance, has `file` and `baseFile` props// expected to return - a String holding the processed asset textdef process(inputText, assetFile) {// ex processor to turn the asset into all UPPER CASEreturn inputText.toUpperCase()}}
// MyAssetPipelineGrailsPlugin.groovyimport asset.pipeline.AssetHelperimport asset.pipeline.MyAssetFileclass MyAssetPipelineGrailsPlugin {// …def doWithDynamicMethods = { ctx ->AssetHelper.assetSpecs << MyAssetFile}}
// scripts/_Events.groovyeventAssetPrecompileStart = { assetConfig ->assetConfig.specs << 'asset.pipeline.MyAssetFile'}
// AngularJS magic injects $window using arg name// MINIFICATION UNSAFEmyApp.controller('MyCtrl', function($window) {$window.alert("Hello World");});// SAFE for minificationmyApp.controller('MyCtrl', ['$window', function($window) {$window.alert("Hello World");}]);
Either turn variable mangling off in Config.groovy or
use Craig Burke's angular-annotate-asset-pipeline plugin
// original: /grails-app/assets/templates/my-app/app-section/index.tpl.htm// <h1>Hello World!</h1>angular.module('myApp.appSection').run(['$templateCache', function($templateCache) {$templateCache.put('index.htm', '<h1>Hello World!</h1>');}]);
$templateCache<r:require modules="app"/><asset:stylesheet href="app.css"/><asset:javascript src="app.js"/><r:external uri="/img/favicon.ico"/><asset:link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/><g:img dir="img" file="logo.jpg" alt="The Logo" /><asset:image src="logo.png" alt="The Logo"/><r:script disposition="defer">alert('foo');</r:script><r:layoutResources/><asset:script>alert('foo');</asset:script><asset:deferredScripts/>href="${resource(dir: 'css', file: 'errors.css')}"href="${asset.assetPath(src: 'errors.css')}"