Go has two template packages: a HTML template package that can be used to templatize HTML files and a more general text template.
The Go text template package can be used easily to generate custom code like Javascript, Swift, React etc.
https://golang.org/pkg/text/template/
This package can be very useful for building products for varied customers that are similar but require their own company logo, names, images and text branding.
In fact, this can be implemented in any codebase that is very similar but needs a few tweaks to work for different platforms or products.
I have used Go text templates for code generation in the following scenarios:
- Generating multiple Chromium, Firefox, Edge extensions for varied products.
- Interfacing with different API’s to get data into our main systems
I’m also working on using it to auto generate custom mobile apps.
Here, I will explain in detail how I build new Chrome and Firefox extensions in seconds using Go templates. This can be expanded to include Microsoft Edge extensions as well.
Start with a basic chrome extension
There is a detailed description on how to build a chrome extension.
https://developer.chrome.com/extensions/getstarted
Once you have a skeletal chrome extension, it is easy to duplicate and create multiple custom extensions if needed or even build Firefox extension using the same set of extension files.
All you need is a set of template javascript files for your extension and your config json values.
Templatize your extension
I start out by taking my extension file and saving with a .template extension. This way, I can recognize it needs to be parsed to replace values.
Files that do not need have any customizations can be left as is in the folder.
E.g. The chrome extension manifest.json.template file will look like this with template fields:
{
"name": "{{.Name}}",
"version": "{{.Version}}",
"manifest_version": 2,
"default_locale": "en",
"description": "{{.Description}}",
"background": {
"page": "background.html"
},
"browser_action": {
"default_title": "{{.Tooltip}}",
"default_icon": "icon.png"
},
"content_scripts": [
{
"js": ["contentscript.js"]
}
],
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"homepage_url": "{{.Protocol}}://{{.Domain}}",
"permissions": [ "tabs",
"{{.Protocol}}://{{.Domain}}/"
]
}
Similarly we write templates file for all the extension files like popup.js etc.
To build a basic chrome extension, I also define the following in a global.json file. This is external to your extension files and will be used by the Go build system.
{
"production": "true",
"author": "Maria De Souza",
"author_home": "https://mariadesouza.com/",
"protocol": "https",
"version" : "0.0.0.1",
"domain" : "www.mysite.com",
"name" : "testExtension",
"description" : "This is a test extension",
"tooltip":"Click here to view settings",
"title" : "Test extension",
}
Gather Customized Settings
I create a folder per customer product where I keep the images related to the customer. This source folder is used later when creating the extension.
The global settings can be overridden by a product specific json file:
{
"title" : "My cool new extension",
"version" : "0.0.0.2",
"domain" : "www.myotherwebsite.com",
}
The customized product.json can be a subset of the original global.json.
Write the build Script
Now we can get to the fun part, building a script to generate the extensions.
Templates are executed by applying them to a data structure. We first define a struct to Unmarshal our config JSON and use it in our build script. Notice the JSON tags correspond to the JSON field names in the global.json and product.json file.
type Config struct {
Production string `json:"production,omitempty"`
Author string `json:"author,omitempty"`
AuthorHome string `json:"author-home,omitempty"`
Version string `json:"version,omitempty"`
Domain string `json:"domain,omitempty"`
Protocol string `json:"protocol,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Tooltip string `json:"tooltip,omitempty"`
Title string `json:"title,omitempty"`
Browser string `json:"browser,omitempty"`
ProductDir string `json:"product_dir,omitempty"`
HomePage string `json:"home_page,omitempty"`
}
The script starts by unmarshalling the global file in a struct value as below. Note that I have left out error handling to reduce noise. The second part will Unmarshal the custom product values.
var globalConfig Config
configFile, _ := ioutil.ReadFile("global.json")
json.Unmarshal(configFile, &globalConfig)
var productConfig Config
productconfigFile,_ := ioutil.ReadFile("product.json") json.Unmarshal(productconfigFile, &productConfig)
Using reflect, I override the custom product values:
func mergeWithGlobal(globalConfig, productConfig *Config){
st := reflect.TypeOf(*globalConfig)
for i := 0; i < st.NumField(); i++ {
tag := strings.Split(field.Tag.Get("json"), ",")
v2 :=
reflect.ValueOf(globalConfig).Elem().FieldByName(st.Field(i).Name)
if tag[0] != "" && v2.String() != "" {
v := reflect.ValueOf(productConfig).Elem().FieldByName(st.Field(i).Name)
v.SetString(v2.String())
}
}
}
Using this Config struct, I can now populate the template files. To do this I read all files with extension .template in the source diectory, execute the template using the populated Config struct and save the result in the destination directory.
Here is what the code would like. Again, I removed some error handling so the main flow is understandable. But error handling should definitely be part of your code.
func populateTemplateFiles(source, dest string, config *Config) error {
//Make destination directory
os.MkdirAll(destination, 0755)
re := regexp.MustCompile(`(.*)\.template$`)
files, _ := ioutil.ReadDir(source)
for _, file := range files {
//if it is a template file read and populate tags
filename := file.Name()
if re.MatchString(filename) == true {
buf, _ := ioutil.ReadFile(filepath.Join(source, filename))
tmpl, _ := template.New("extensions").Parse(string(buf))
// final file will drop the .template file ext
targetfilename := strings.Split(filename, ".template")
destFile := targetfilename[0]
targetfile := filepath.Join(dest, destFile)
f, err := os.OpenFile(targetfile, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
fmt.Println("Failed to create file", targetfile, err)
continue
}
w := bufio.NewWriter(f)
tmpl.Execute(w, config)
w.Flush()
} else {
// not a template file - copy as is
copyFile(filepath.Join(dest, filename), filepath.Join(source, filename))
}
}
return nil
}
The customized images in the product directory are copied into the destination directory.
The destination directory will then contain all the customized javascript, other javascript files and custom images.
We can then upload a zipped version of the destination directory to the Chrome Webstore. I have a make file that also generates a pem file and zips the directory.
Extending build system for Firefox
I build Firefox extensions from the same set of Chrome extension files.
This is the link to developing a Firefox browser extension:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
Firefox and Chrome extensions have very subtle differences, in the way they are implemented. They are all based largely on the WebExtensions API for cross browser compatibility.
Notice that my earlier declaration of Config struct had the following field
Browser string `json:"browser,omitempty"`
I dynamically change this in my Go script to instruct the system to generate a new set of extension files for Firefox.
I set the value for Browser in my code and then build for Firefox.
productConfig.Browser = "Firefox"
In fact, you can parameterize the Go script to accept a command line argument that will control which browser you want to build for. The default could be all.
The differences between Firefox and Chrome will be conditionals in your template files. It depends on what functionality you are using within your extensions.
E.g. to send a message back to the background script from the content script, I use the template system to conditionally generate code for Firefox vs Chrome using the config value.
{{if eq .Browser "Firefox"}}
browser.runtime.sendMessage({ name: "options", value: event.target.value });
{{else}}
chrome.extension.sendRequest({ name: "options", value: event.target.value });
{{end}}
Another example of this would be, add FirefoxID in my config struct and in my manifest.json.template, I add
{{if eq .Browser "Firefox"}}
"applications": {
"gecko": {
"id": "{{.FirefoxID}}",
"strict_min_version": "42.0",
}
},
{{end}}
Microsoft Edge extensions are also similar and the same set of template files can be used to auto-generate Edge with a few tweaks.