Using Polymer with Closure Compiler - Part 3: Renaming in Templates

Wednesday, June 22, 2016 | 4:48 AM

This is the last post in a 3-part series about using Polymer with Closure Compiler

With Closure-Compiler in ADVANCED mode, the concept of “whole world” optimization is used. Simply stated, the compiler needs to know about all of the JavaScript source used and all the ways it can be consumed by other libraries/event handlers/scripts.

Polymer templates would logically be thought of as an external use case. However, symbols referenced externally can't be renamed by the compiler. So, we need to provide the Polymer templates to Closure-Compiler along with our script source so that everything can be renamed consistently. Problem is, Polymer templates are HTML not JavaScript.

Polymer-rename was created to solve just this problem. It works by translating the HTML template data-binding expressions to JavaScript before compilation and then reverses the process afterwards.

Before Compilation: Extracting Expressions

The polymer-rename extract plugin parses the HTML of a Polymer element. It ignores the content of <script> and <style> tags. It looks for polymer expressions such as:

<button on-tap="tapped_">[[buttonName]]</button>

From these expressions, it generates matching JavaScript:

polymerRename.eventListener(16, 23, this.tapped_);
polymerRename.symbol(27, 37, this.buttonName);

This JavaScript is not designed to ever be executed directly. You don't package it with your elements. Instead, Closure-Compiler uses this to consistently rename the symbols.

Compiling: Separate the Output

To separate your Polymer element script source from the templates and to provide all the source to Closure-Compiler in the correct order, use the vulcanize tool to combine all of your elements and inline all of the scripts. Next, use the crisper tool to extract all of your inline scripts into a single external javascript file. If you want your script inlined after compilation, just use the vulcanize tool again.

With your polymer-rename generated script and your extracted source from vulcanize and cripser, you can now use Closure-Compiler. By default, Closure-Compiler is going to combine all of your JavaScript into a single output file. But we do not want the polymer-rename generated code packaged with the rest of our scripts. Closure-Compiler has a powerful - yet confusing to use - code splitting functionality which will allow us to direct some JavaScript to a different output file. The confusing part is that the flag to trigger the code splitting is named “module”. Don't mistake this with input module formats like ES6, CommonJS or goog.module - it has nothing to do with these.

Here's an example compile command (using the compiler gulp plugin):

const closureCompiler = require('google-closure-compiler');

gulp.task('compile-js', function() {
  gulp.src([
        './src/js/element-source.js',
        './build/element-template-expressions.js'])
      .pipe(closureCompiler({
        compilation_level: 'ADVANCED',
        warning_level: 'VERBOSE',
        polymer_pass: true,
        module: [
          'element-source:1',
          'element-template-expressions:1:element-source'
        ],
        externs: [
          require.resolve(
              'google-closure-compiler/contrib/externs/polymer-1.0.js'),
          require.resolve(
              'polymer-rename/polymer-rename-externs.js')
        ]
      })
      .pipe(gulp.dest('./dist'));
});

What's going on here? We provided exactly two javascript files to Closure-Compiler - and the order matters greatly. The first module definition consumes 1 javascript file (that's the :1 part of the flag). The second module flag also consumes 1 javascript file and logically depends on the first module. The code-splitting flags are a bit unwieldy as they require you to have an exact count of your input files and to make sure they are apportioned between your module flags correctly - and in the right order.

After compilation completes, the “dist” folder should have two javascript files: element-source.js and element-template-expressions.js. The element-template-expressions.js file should only contain the template expressions extracted by the polymer-rename project, but now with all of the symbol references properly renamed.

After Compilation: Updating the Templates

Now it's time to go back and update the original HTML templates with our newly renamed expressions. There's not a lot to this step - just call the polymer-rename replace plugin and watch it work. The example Polymer HTML expression from earlier might now look something like:

<button on-tap="a">[[b]]</button>

Custom Type Names

In part 1 of the series, I discussed how the Polymer pass of Closure-Compiler generates type names based off the element tag name: <foo-bar> by default will have the type name FooBarElement. However, I also explained that an author can assign the return value of the Polymer function to specify custom type names. The polymer-rename plugins will use the same logic to determine type names. If any of your elements have custom type names you will need to provide those names to the extract plugin of polymer-rename.

The extract plugin optionally takes a function which is used to lookup these names. Here's an example implementation of a custom lookup function:

/**
 * Custom element type name lookup
 * @param {string} tagName
 * @return {string|undefined}
 */
function lookupTypeByTagName(tagName) {
  if (/^foo(-.*)/.test(tagName)) {
    return 'myNamespace.Foo' + tagName.replace(/-([a-z])/g,
        function(match, letter) {
      return letter.toUpperCase();
    });
  }

  // returning undefined here causes the polymer-rename
  // plugin to fall back to the default
  // behavior for type name lookups.
  return undefined;
}

In this implementation, any element that starts with foo- will have a type name that is upper camel case and a member of the myNamespace object.

Summary

In addition to allowing full renaming of Polymer elements by Closure-Compiler, the polymer-rename plugin also enables a wide range of type checking. The compiler can now see how Polymer computed property methods are called - and will properly notify you if the number of arguments or types don't match.

Closure-Compiler ADVANCED optimizations and Polymer can create a powerful app, it just takes a little work and an understanding of how they fit together.

1 comments:

Johann Blake said...

Will this work with Polymer 2.0?