Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import asset.pipeline.AbstractProcessor
import asset.pipeline.AssetCompiler
import asset.pipeline.AssetFile
import asset.pipeline.AssetPipelineConfigHolder
import asset.pipeline.DirectiveProcessor
import org.springframework.web.util.UriComponents
import org.springframework.web.util.UriComponentsBuilder
import java.util.regex.Pattern
import static asset.pipeline.AssetHelper.getByteDigest
/**
* A processor to process JS files and replace links to .html or .htm files with the digested version of the file.
* Will match links both with and without a sub extension:
* `someFile.tpl.html`
* `someFile.tpl.htm`
* `someFile.html`
* `someFile.anysubextension.html`
* `../../someFile.tpl.html'
*
* Throws an exception if the replacement file cannot be resolved via AssetPipelineConfigHolder.resolvers
*/
class AngularTemplateJsProcessor extends AbstractProcessor {
static
final Pattern TEMPLATE_INCLUSION_PATTERN = ~/(['"]+?)((?:.\/)|(?:\.\.\/)+)?([a-zA-Z0-9\-_:\/@#? &+%=]+?)(\.[a-zA-Z]+)?(\.html|\.htm)(['"]+?)/
List<String> INCLUDED_DIRS = ["shared", "client", "merchants"]
AngularTemplateJsProcessor(AssetCompiler precompiler) {
super(precompiler)
}
/**
* Do not process external libraries like angular and bootstrap
* @param assetFile
* @return
*/
boolean shouldProcessFile(AssetFile assetFile) {
UriComponents uri = UriComponentsBuilder.fromPath(assetFile.getPath()).build()
String containingDir = uri.getPathSegments().first()
boolean include = INCLUDED_DIRS.contains(containingDir)
if (!include) {
log("Skipping asset [${assetFile.getPath()}] as it is not in one of the included dirs [${INCLUDED_DIRS}]")
}
return include
}
String process(final String inputText, final AssetFile assetFile) {
if (precompiler?.options?.enableDigests) {
//Only done in production mode
println("[${AngularTemplateJsProcessor.class.simpleName}] Precompilation enabled.")
if (shouldProcessFile(assetFile)) {
log("Processing file: ${assetFile.getPath()}")
final Map<String, String> cachedPaths = [:]
return inputText.replaceAll(TEMPLATE_INCLUSION_PATTERN) {
final String original,
final String openingQuote,
final String relativePathMarkers, // ../../ or ./
final String targetLocation,
final String optionalSubExtension,
final String extension,
final String closingQuote ->
String assetPrefix = "${relativePathMarkers ?: ''}${targetLocation}${optionalSubExtension ?: ''}"
String targetAssetLocation = assetPrefix + extension
log("Attempting to replace the templateUrl path to: [" + targetAssetLocation + "]")
final String cachedPath = cachedPaths[targetAssetLocation]
if (!cachedPath) {
final AssetFile targetAsset = tryFind(targetAssetLocation)
assert targetAsset: "Could not locate the asset [${targetAssetLocation}]"
String resolvedLocation = assetPrefix + '-' + getAssetDigest(targetAsset, precompiler) + extension
cachedPaths.put(targetAssetLocation, resolvedLocation)
}
String digestedPath = "${openingQuote}${cachedPaths.get(targetAssetLocation)}${closingQuote}"
log("Replacing path: [${targetAssetLocation}] with: [${digestedPath}]")
return digestedPath
}
}
}
return inputText
}
private static void log(String msg) {
println("[${AngularTemplateJsProcessor.class.simpleName}]: ${msg}")
}
private static String getAssetDigest(AssetFile assetFile, AssetCompiler precompiler) {
//assetFile.getByteDigest() does not match what asset.pipeline.AssetHelper.getByteDigest produces
return getByteDigest(new DirectiveProcessor(assetFile.contentType[0], precompiler).compile(assetFile).bytes)
}
private static AssetFile tryFind(String assetPath) {
for (resolver in AssetPipelineConfigHolder.resolvers) {
AssetFile file = resolver.getAsset(assetPath)
if (file) {
return file
}
}
return null
}
}
import asset.pipeline.AssetCompiler
import asset.pipeline.AssetPipelineConfigHolder
import asset.pipeline.JsAssetFile
import asset.pipeline.fs.AssetResolver
import asset.pipeline.fs.FileSystemAssetResolver
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Ignore
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
import java.util.regex.Matcher
class AngularTemplateJsProcessorSpec extends Specification {
@Rule
public TemporaryFolder testDir = new TemporaryFolder()
@Shared
AssetResolver resolver
@Shared
File baseDir
@Shared
File compileDir
def setup() {
baseDir = testDir.newFolder('assets', 'javascripts').parentFile
compileDir = testDir.newFolder('target')
testDir.newFolder('assets', 'javascripts', 'client')
testDir.newFolder('assets', 'javascripts', 'template', 'client')
resolver = new FileSystemAssetResolver('application', baseDir.absolutePath)
AssetPipelineConfigHolder.resolvers.add(resolver)
}
@Unroll
def "template inclusion patterns should match [#line]"() {
when:
Matcher matcher = line =~ AngularTemplateJsProcessor.TEMPLATE_INCLUSION_PATTERN
then:
matcher
joinMatchedFilePath(matcher) == expectedCapture
where:
line | expectedCapture
'templateUrl: "/client/templ/some-file.html"' | '/client/templ/some-file.html'
'templateUrl: "/client/templ/some.html"' | '/client/templ/some.html'
'templateUrl: "/client/templ/some.erb.html"' | '/client/templ/some.erb.html'
'"/client/templ/some.tpl.html"' | '/client/templ/some.tpl.html'
'templateUrl: "/client/templ/some.tpl.html"' | '/client/templ/some.tpl.html'
'templateUrl: "../client/templ/some.tpl.html"' | '../client/templ/some.tpl.html'
'templateUrl: "../../client/templ/some.tpl.html"' | '../../client/templ/some.tpl.html'
'templateUrl: "./client/templ/some.tpl.html"' | './client/templ/some.tpl.html'
'templateUrl: "/client/templ/some_.tpl.html"' | '/client/templ/some_.tpl.html'
'templateUrl:"/client/templ/some.tpl.html"' | '/client/templ/some.tpl.html'
"templateUrl:'/client/templ/some.tpl.html'" | '/client/templ/some.tpl.html'
'templateUrl: templatePath("content.tpl.html"),' | 'content.tpl.html'
'nuts templateUrl: templatePath("content.tpl.html") and gum' | 'content.tpl.html'
'nuts templateUrl: templatePat"content.tpl.html") and gum' | 'content.tpl.html'
' return templatePath("template/form-element.tpl.html");' | 'template/form-element.tpl.html'
' return templatePath("template/form-element-v2.tpl.html");' | 'template/form-element-v2.tpl.html'
}
@Unroll
def "template inclusion does not match other urls"() {
when:
Matcher matcher = line =~ AngularTemplateJsProcessor.TEMPLATE_INCLUSION_PATTERN
then:
!matcher
where:
line << [
'templateUrl: "/client/templ/some.css"',
'templateUrl:"/client/templ/some.js"',
"templateUrl:'/client/templ/some.htmls'"]
}
def "a js file referencing an angular template is replaced with the path to the digested asset"() {
String includedFileName = 'content.tpl.html'
setup:
htmlFile(includedFileName)
String jsFileName = 'someFile.js'
jsFile(jsFileName, includedFileName)
maybeAddAngularTemplateJsProcessor()
when:
new AssetCompiler([compileDir: compileDir.absolutePath, enableDigests: true]).compile()
then:
Properties props = new Properties()
props.load(new File(compileDir, "manifest.properties").newDataInputStream())
File digestedJsFile = new File(compileDir, props.get("client/${jsFileName}".toString()) as String)
String replacedPath = props.get("template/client/${includedFileName}".toString())
replacedPath ==~ /template\/client\/content.tpl-[a-zA-Z0-9]{32}.html/
digestedJsFile.text.trim() == """
return {
templateUrl: "${replacedPath}",
scope: {
loyaltyCard: "=",
legacyLoyalty: "=",
favourite: "="
},
""".trim()
}
@Ignore
/**
* Fails for .html files without a sub-extension
* e.g:
* java.lang.AssertionError: Could not locate the asset [template/client/content-1f3a38ca383637a6c7ef3809b3a6c275.html].
* Expression: targetAsset. Values: targetAsset = null
* at au.com.sensis.zinc.assets.AngularTemplateJsProcessor.process_closure1(AngularTemplateJsProcessor.groovy:73)
* at groovy.lang.Closure.call(Closure.java:414)
* at groovy.lang.Closure.call(Closure.java:430)
*
*/
def "a js file referencing a html asset is replaced with the path to the digested asset"() {
String includedFileName = 'content.html'
setup:
htmlFile(includedFileName)
String jsFileName = 'someFile.js'
jsFile(jsFileName, includedFileName)
maybeAddAngularTemplateJsProcessor()
when:
new AssetCompiler([compileDir: compileDir.absolutePath, enableDigests: true]).compile()
then:
Properties props = new Properties()
props.load(new File(compileDir, "manifest.properties").newDataInputStream())
File digestedJsFile = new File(compileDir, props.get("client/${jsFileName}".toString()) as String)
String replacedPath = props.get("template/client/${includedFileName}".toString())
replacedPath ==~ /template\/client\/content-[a-zA-Z0-9]{32}.html/
digestedJsFile.text.trim() == """
return {
templateUrl: "${replacedPath}",
scope: {
loyaltyCard: "=",
legacyLoyalty: "=",
favourite: "="
},
""".trim()
}
def "does not replace with the digested path when in dev mode"() {
String includedFileName = 'content.tpl.html'
setup:
htmlFile(includedFileName)
jsFile('someFile.js', includedFileName)
maybeAddAngularTemplateJsProcessor()
when:
new AssetCompiler([compileDir: compileDir.absolutePath, enableDigests: false]).compile()
then:
File digestedJsFile = new File(compileDir, 'client/someFile.js')
digestedJsFile.text.trim() == """
return {
templateUrl: "template/client/${includedFileName}",
scope: {
loyaltyCard: "=",
legacyLoyalty: "=",
favourite: "="
},
""".trim()
}
private File htmlFile(String name) {
File f = testDir.newFile("assets/javascripts/template/client/${name}")
f << '''
<html>
<body>
<p>hello</p>
</body>
</htm/>
'''
return f
}
private File jsFile(String name, String templateFileName) {
File f = testDir.newFile("assets/javascripts/client/${name}")
f << """
return {
templateUrl: "template/client/${templateFileName}",
scope: {
loyaltyCard: "=",
legacyLoyalty: "=",
favourite: "="
},""".trim()
return f
}
private static void maybeAddAngularTemplateJsProcessor() {
if (!JsAssetFile.processors.contains(AngularTemplateJsProcessor)) {
JsAssetFile.processors.add(AngularTemplateJsProcessor)
}
}
private static String joinMatchedFilePath(Matcher matcher) {
(matcher.group(2) ?: '') + matcher.group(3) + (matcher.group(4) ?: '') + matcher.group(5)
}
}
@adrianbk

This comment has been minimized.

Copy link
Owner Author

commented Jun 12, 2017

Grails config to add the processor: JsAssetFile.processors.add(AngularTemplateJsProcessor)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.