Created
June 12, 2017 08:07
-
-
Save adrianbk/6cc7916996937dcbe99360cca62fc740 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Grails config to add the processor:
JsAssetFile.processors.add(AngularTemplateJsProcessor)