Golang

Auto-generate code using Go templates

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:

  1. Generating multiple Chromium, Firefox, Edge extensions for varied products.
  2. 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.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s