Swig Templating in Chrome Packaged Apps

tl;dr Check out the sample code on github

I’ve been working on a Chrome Packaged App (Insomnia REST Client) for the past couple months and have leaned a lot. One of the biggest challenges about Packaged Apps is working around the Content Security Policy (CSP).

The CSP makes it difficult to do common tasks such as accessing remote resources, embedding iframes, or using templating libraries.

Today, I’m going to demonstrate how to use a sandboxed iframe and message passing to achieve template rendering with the Swig templating engine.

Note: if you don’t need to live-render templates in-app then it is better to precompile your templates and include them in your app source. The following sandboxing workaround is only necessary if you need to render dynamic content or user-generated templates in-app.

Overview

Due to the CSP, most templating engines are not allowed to execute in a Chrome Packaged App because of the use of eval(). Sandboxed pages, however, are allowed to use eval(). This means that rendering is possible, and we just need an easy way to communicate back and forth with the sandbox. Luckily this isn’t that hard.

Packaged Apps are allowed to communicate with sandboxes via message passing. Message passing allows the sending objects from one environment to another. We’re going to leverage this feature to create a two-way rendering helper that works as follows:

  • app passes content to render to the iframe (as a message)
  • iframe script receives message, renders content
  • iframe script passes new message to parent with rendered content

These steps are shown in the illustration below:

Swig Chrome Packaged App

Alright, let’s get coding! Remember, you can view the full sample code on Github.

  1. Specify the Sandboxed Page —————————–

manifest.json is a metadata file that defines the properties of a Chrome Packaged App (or extension). The only specific thing we need to include here is the sandbox attribute.

/** manifest.json */

"sandbox": {
    // define render.html as a sandboxed page
    "pages": [ "render.html" ]
}
  1. Include Sandboxed iframe —————————–

In our app’s main HTML page (index.html), we need to include the sandboxed page as an invisible iframe. We can then use the iframe to execute the unsafe Swig template code.

<!-- index.html -->

<!-- Use an iframe as a sandbox. This is what we'll use to render -->
<iframe src="render.html" id="sandbox" style="display:none"></iframe>

<!-- Include some sample JS to communicate with the sandbox -->
<script src="index.js"></script>
  1. Render Swig Template from Sandbox ————————————

Now lets jump over to our sandboxed environment. The script below shows the code needed to listen for messages from the parent, and render a template.

<!-- render.html -->

<script src="swig.js"></script>

<script>
    // Listen for messages from within the iframe
    window.addEventListener('message', function (event) {

        // Render the content with the passed context
        var content = swig.render(event.data.template, {
            locals: event.data.context
        });

        // Send a message back to the parent that sent it
        event.source.postMessage({
            content: content,
        }, event.origin);
    });
</script>

Pretty simple right?

  1. Call the Sandboxed Code ————————–

Now that we have sandboxed code to handle and render a message, lets call the code from our app.

/** index.js */

window.onload = function () {
    // Store current callback where it can be referenced
    var globalCallback;

    /**
     * Handy helper function that we can call to render.
     * This wraps the clunky two-way message passing into a
     * friendly callback interface.
     */
    function render (template, context, callback) {
        globalCallback = callback;

        // Grab the iframe sandbox
        var iframe = document.getElementById('sandbox');

        // Put together a message to pass
        var message = { template: template, context: context };

        // Send a message to sandbox with content to render
        iframe.contentWindow.postMessage(message, '*');
    }

    // Listen for messages that come back after rendering done
    window.addEventListener('message', function (event) {
        globalCallback(event.data.content);
    });
};
  1. Test it Out! —————

Now lets try out our render() method.

/** Call our render() function */

render('foo --> {{ foo }}', { foo: 'bar' }, function (content) {
    document.body.innerHTML = content;
});
  1. Load it Into Chrome ———————-

That’s it. You can download and run the sample code in Chrome by following these steps:

  • navigate to chrome://extensions/
  • enable delevoper mode
  • open project with Load unpacked extension

Final Thoughts

This post covered a basic implementation of using message passing to render a Swig template. The implementation that I use for Insomnia is a bit more robust, but uses the same principles.

If you want me to go more in depth on improving this code let me know on Twitter.

Thanks for reading!

tutorial chrome app