Rename polymer template databinding expressions and event functions with closure-compiler
npm install polymer-renameUpdated for Polymer 2
Closure-compiler with ADVANCED optimizations, offers the powerful
ability to rename properties. However, it can only safely be used on code bases which follow its
required conventions.
With the introduction of polymer templates, data binding expressions become external uses of code which the compiler
must be made aware of or optimizations such as dead-code elimination and property renaming will break the template
references. While it's possible to quote or export all of the properties referenced from polymer templates, that action
significantly decreases both the final compression as well as the type checking ability of the compiler.
This project offers an alternative approach. Prior to compilation with Closure-compiler, the Polymer template html
is parsed and any data binding expressions are extracted. These extracted expressions are then output in an alternative
form as javascript. This extracted javascript is never intended to be actually executed, but it is provided to
Closure-compiler during compilation. The compiler can then safely rename references and thus the need to export or
quote properties used in data-binding is eliminated.
In addition, considerable type checking is enabled for data-binding expressions.
Original
``html`
After Compilation/Renaming
`html`
For Polymer 1, Closure-Compiler blocked renaming of any declared property. With Polymer 2, the compiler now uses
the standard conventions: quoted properties are not renamed and other properties are. Declared properties
with the reflectToAttribute or readOnly properties will never be renamed.
In addition, this project will rename attributes which map to properties of a custom element.
This project functions in two phases: pre and post closure-compiler compilation. Each phase has its own gulp plugin
or can be used as native JS functions.
This first phase of the project parses polymer element templates and produces a javascript file to pass to
closure-compiler.
The plugin makes use of the polymer-analyzer and requires both the
HTML templates as well as any external JS source that contains element definitions.
Gulp
`js
const gulp = require('gulp');
const polymerRename = require('polymer-rename');
gulp.task('extract-data-binding-expressions', function() {
gulp.src('/src/components/*/.html') // Usually this will be the bundled file - may also need to add .js files
.pipe(polymerRename.extract({
outputFilename: 'foo-bar.template.js'
}))
.pipe(gulp.dest('./build'));
});
`
Closure-compiler's code-splitting flags allow the output to be divided into separate files. The compilation must
reference the extern file included with this package.
In addition, the compiler can now warn about mismatched types, misspelled or missing property references and other
checks.
`js
let closureCompiler = require('google-closure-compiler');
gulp.task('compile-js', function() {
gulp.src(['./src/js/app.js', './build/foo-bar.template.js'])
.pipe(closureCompiler({
compilation_level: 'ADVANCED',
warning_level: 'VERBOSE',
polymer_pass: true,
module: [
'app:1',
'foo-bar.template:1:app'
],
externs: [
require.resolve('google-closure-compiler/contrib/externs/polymer-1.0.js'),
require.resolve('polymer-rename/polymer-rename-externs.js')
]
})
.pipe(gulp.dest('./dist'));
});
`
After compilation, the compiler will have consistently renamed references to the properties. The generated javascript
contains indexes into the original template which will now be replaced with their renamed versions.
Gulp
`js
const polymerRename = require('polymer-rename');
gulp.task('update-html-template', function() {
gulp.src('./src/components/foo-bar.html') // Usually this will be the bundled file
.pipe(polymerRename.replace('./dist/foo-bar.template.js'))
.pipe(gulp.dest('./dist/components'));
});
`
polymer-rename obtains the type names of elements from polymer-analyzer. Type names for
elements must be global.
Giving the following polymer template, the first phase of the project will extract the
data-binding expressions and create a valid JS file:
`html`
[[formatName(name)]]
[[employer]]
[[item.street]]
[[item.city]], [[item.state]] [[item.zip]]
Generated JavaScript from the extracted data-bound properties:
`js`
(/* @this {FooBarElement} / function() {
polymerRename.eventListener(72, 83, this.nameClicked);
polymerRename.identifier(98, 102, this.name);
this.formatName(this.name);
polymerRename.method(87, 97, this.formatName);
polymerRename.identifier(123, 131, this.employer);
polymerRename.identifier(179, 188, this.addresses);
for (let index = 0; index < this.addresses.length; index++) {
let item = this.addresses[index];
polymerRename.identifier(206, 217, item.street, item, 'item');
polymerRename.identifier(239, 248, item.city, item, 'item');
polymerRename.identifier(254, 264, item.state, item, 'item');
polymerRename.identifier(269, 277, item.zip, item, 'item');
}
}).call(/* @type {FooBarElement} / (document.createElement("foo-bar")))
Each of the special function calls is defined as an extern. Each call takes a pair of
indexes where the expression resides in the original file. After compilation, these indexes are used to replace the
original expression with the now renamed properties and methods.
The compiler consumes the JS file and outputs the renamed expressions:
`js`
(function() {
polymerRename.eventListener(72, 83, this.a);
polymerRename.identifier(98, 102, this.b);
this.c(this.b);
polymerRename.method(87, 97, this.c);
polymerRename.identifier(123, 131, this.d);
polymerRename.identifier(179, 188, this.e);
for (let a = 0; a < this.e.length; a++) {
let b = this.e[a];
polymerRename.identifier(206, 217, b.f, b, 'item');
polymerRename.identifier(239, 248, b.g, b, 'item');
polymerRename.identifier(254, 264, b.h, b, 'item');
polymerRename.identifier(269, 277, b.i, b, 'item');
}
}).call(document.createElement("foo-bar"))
The provided indexes are now used to update the original Polymer template.
Several functions and options in Polymer require providing property names as strings. This is problematic as
Closure-Compiler does not rename strings. This would include the following features:
* Computed properties
* Observers
* Polymer 1 Listeners
* notifyPath
* notifySplices
* set
To work around such limitations, Closure-Compiler recognizes two special functions defined in Closure-Library.
If your project does not utilize Closure-Library, you can simply copy the definitions for these two
functions to your code base. As long as they are named the same, the compiler will recognize them.
goog.reflect.objectProperty
returns a renamed string for an object instance. It's particularly helpful when calling notifyPath, notifySplicesset
or .
`js`
this.notifyPath(goog.reflect.objectProperty('foo', this), this.foo);
goog.reflect.object
renames the keys of an object literal consistently with a provided constructor. It's useful when an instance
of the object is not available.
`js
// If using closure-library, this function is goog.object.transpose
function swapKeysAndValues(obj) {
let swappedObj = {};
Object.keys(obj).map(key => {
swappedObj[obj[key]] = key;
});
return swappedObj;
}
var myCustomElementProps = swapKeysAndValues(
goog.reflect.object(MyCustomElement, {
_fooChanged: '_fooChanged'
})
);
var MyCustomElement = Polymer({
is: 'my-custom',
properties: {
foo: {
type: Boolean,
observer: myCustomElementProps['_fooChanged']
}
},
_fooChanged: function(newValue, oldValue) {}
});
``
Properties which are quoted are no longer renamed. However sub-paths are not analyzed. If the a property subpath
should be blocked from renaming, use extern types to ensure that the compiler will not rename the paths.