Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve .less handling and compiling #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.swp
stacktrace.log
target/*
web-app/WEB-INF/tld/*

15 changes: 15 additions & 0 deletions LesscssResourcesGrailsPlugin.groovy
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.grails.plugin.resource.ResourceService
import org.grails.plugin.resource.ResourceTagLib

class LesscssResourcesGrailsPlugin {
// the plugin version
def version = "0.6.1"
Expand All @@ -21,4 +24,16 @@ This plugin supports server-side compilation of .less CSS files to their .css co

// URL to the plugin's documentation
def documentation = "https://github.com/paulfairless/grails-lesscss-resources"

def doWithSpring = { ->
// monkey-patch ResourceService and ResourceTagLib to support .less files
ResourceService.DEFAULT_MODULE_SETTINGS['less'] = [
disposition: 'head'
]
ResourceTagLib.SUPPORTED_TYPES['less'] = [
type: "text/css",
rel: 'stylesheet/less',
media: 'screen, projection'
]
}
}
39 changes: 27 additions & 12 deletions grails-app/resourceMappers/LesscssResourceMapper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import org.grails.plugin.resource.mapper.MapperPhase
*
* Mapping file to compile .less files into .css files
*/
import org.codehaus.groovy.grails.plugins.support.aware.GrailsApplicationAware
import org.codehaus.groovy.grails.commons.GrailsApplication

class LesscssResourceMapper implements GrailsApplicationAware {
class LesscssResourceMapper {

GrailsApplication grailsApplication
def grailsApplication
def resourceService

def phase = MapperPhase.GENERATION // need to run early so that we don't miss out on all the good stuff
def operation = "compile"

static defaultExcludes = ['**/*.js','**/*.png','**/*.gif','**/*.jpg','**/*.jpeg','**/*.gz','**/*.zip']
static defaultIncludes = ['**/*.less']
static String LESS_FILE_EXTENSION = '.less'

def map(resource, config){
Expand All @@ -25,34 +26,48 @@ class LesscssResourceMapper implements GrailsApplicationAware {

if (resource.sourceUrl && originalFile.name.toLowerCase().endsWith(LESS_FILE_EXTENSION)) {
LessEngine engine = new LessEngine()
File input = getOriginalFileSystemFile(resource.sourceUrl);
URL input = getOriginalResourceURLForURI(resource.sourceUrl)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this will work better for a problem I'm facing. I had some trouble with deployments having war files that are unexploded.
http://grails.1312388.n4.nabble.com/lesscss-resources-not-working-in-production-tt4429062.html has the discussion for it.

target = new File(generateCompiledFileFromOriginal(originalFile.absolutePath))

// save current context classloader and set the classloader from the
// Grails application, otherwise we have trouble accessing the original
// resources when reloading
def thread = Thread.currentThread()
def saveCL = thread.contextClassLoader
thread.contextClassLoader = grailsApplication.classLoader

if (log.debugEnabled) {
log.debug "Compiling LESS file [${originalFile}] into [${target}]"
}
try {
engine.compile input, target
target.text = engine.compile(input)

// Update mapping entry
// We need to reference the new css file from now on
resource.processedFile = target
// Not sure if i really need these
resource.updateActualUrlFromProcessedFile()

// Change the source extension so the compiled CSS gets into the
// .css bundle, not a separate .less bundle
resource.sourceUrlExtension = 'css'
resource.actualUrl = generateCompiledFileFromOriginal(resource.originalUrl)
resource.contentType = 'text/css'
// fixup the rel attribute
resource.tagAttributes.rel = 'stylesheet'

} catch (LessException e) {
log.error("error compiling less file: ${originalFile}")
e.printStackTrace()
} finally {
// restore saved classloader
thread.contextClassLoader = saveCL
}
}
}

private String generateCompiledFileFromOriginal(String original) {
original.replaceAll(/(?i)\.less/, '_less.css')
original.replaceAll(/(?i)\.less/, '_less.css')
}

private File getOriginalFileSystemFile(String sourcePath) {
grailsApplication.parentContext.getResource(sourcePath).file
private URL getOriginalResourceURLForURI(String sourceUri) {
resourceService.getOriginalResourceURLForURI(sourceUri);
}
}
4 changes: 2 additions & 2 deletions test/functional/LesscssBundleSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ class LesscssBundleSpec extends GebSpec {
}
def "check lesscss rules rendered"(){
when:
go('http://localhost:8080/lesscss-resources')
go('')
then:
$('h1').text() == 'Less Test'
$('h1').jquery.css('color') == 'rgb(34, 34, 251)'
$('h2').jquery.css('color') == 'rgb(132, 34, 16)'
$('h3').jquery.css('color') == 'rgb(34, 251, 34)'
}
}
}
23 changes: 13 additions & 10 deletions test/unit/LesscssResourceMapperTests.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,63 @@ class LesscssResourceMapperTests extends GroovyTestCase{
void setUp() {
mapper = new LesscssResourceMapper()
mapper.metaClass.log = [debug:{}, error:{}]
mapper.grailsApplication = [ classLoader: Thread.currentThread().contextClassLoader ]
}

@Test
void testMapperGeneratesCssFromLessResource() {
String fileName = "file.less"
def targetFile = mock(File, constructor('/var/file/file_less.css'))
targetFile.setText(match { true }).once()

def originalFile = mock(File)
def originalFile = mock(URL)
def processedFile = mock(File)
processedFile.getName().returns(fileName).stub()
processedFile.getAbsolutePath().returns('/var/file/'+fileName).stub()

def lessEngine = mock(LessEngine, constructor())
lessEngine.compile(originalFile, targetFile).once()
lessEngine.compile(originalFile).once()

def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'less', contentType:'', originalUrl:'file.less', tagAttributes:[rel:'stylesheet/less']]
def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'less', contentType:'', originalUrl:'file.less', tagAttributes:[rel:'stylesheet/less'], updateActualUrlFromProcessedFile: { actualUrl = 'file_less.css' }]
resource.updateActualUrlFromProcessedFile.delegate = resource
def config = [:]

def mockedMapper = mock(mapper)
mockedMapper.getOriginalFileSystemFile(fileName).returns(originalFile)
mockedMapper.getOriginalResourceURLForURI(fileName).returns(originalFile)

play {
mockedMapper.map (resource, config)
assertEquals 'file_less.css', resource.actualUrl
assertEquals 'css', resource.sourceUrlExtension
assertEquals 'stylesheet', resource.tagAttributes.rel
assertEquals 'text/css', resource.contentType
}
}

@Test
void testMapperHandlesUpperCaseFileExtension() {
String fileName = "file.LESS"
def targetFile = mock(File, constructor('/var/file/file_less.css'))
targetFile.setText(match { true }).once()

def originalFile = mock(File)
def originalFile = mock(URL)
def processedFile = mock(File)
processedFile.getName().returns(fileName).stub()
processedFile.getAbsolutePath().returns('/var/file/'+fileName).stub()

def lessEngine = mock(LessEngine, constructor())
lessEngine.compile(originalFile, targetFile).once()
lessEngine.compile(originalFile).once()

def mockedMapper = mock(mapper)
mockedMapper.getOriginalFileSystemFile(fileName).returns(originalFile)
mockedMapper.getOriginalResourceURLForURI(fileName).returns(originalFile)

def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'LESS', contentType:'', originalUrl:'file.LESS', tagAttributes:[rel:'stylesheet/less']]
def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'LESS', contentType:'', originalUrl:'file.LESS', tagAttributes:[rel:'stylesheet/less'], updateActualUrlFromProcessedFile: { actualUrl = 'file_less.css' }]
resource.updateActualUrlFromProcessedFile.delegate = resource
def config = [:]
play {
mockedMapper.map (resource, config)
assertEquals 'file_less.css', resource.actualUrl
assertEquals 'css', resource.sourceUrlExtension
assertEquals 'stylesheet', resource.tagAttributes.rel
assertEquals 'text/css', resource.contentType
}
}

Expand Down