Publishing Polymer Elements Written in TypeScript (With Dependencies)

I love consuming custom elements but writing them in Polymer with ES5 is far from ideal. ES6 (or more correctly ES2015) could offer some improvement but it is still not officially supported by the Polymer team and their toolset.

Thankfully, there is PolymerTS which offers a vastly improved Polymer API, mainly thanks to decorators. It also let’s developers take advantage of ES6 modules but there is one problem: how do you publish elements with dependencies both on JSPM packages and other elements from Bower?

TL;DR;

Here are some highlights from this post:

  1. Don’t reference bower dependencies directly to avoid vulcanizing polymer.html * reference them in package manager-specific entrypoint instead
  2. Use jspm build-sfx to publish for Bower
  3. Use jspm bundle to publish for JSPM

Show me the code already

I’ve created two example repositories:

  1. md-ed - a component written in PolymerTS
  2. its sample usage with Bower and JSPM

Repo setup

Inspired by the Taming Polymer post by Juha Järvi, the initial setup involves preparing JSPM, SystemJS and TypeScript. The original post however, discusses creating apps. Here I will show how to create, publish and consume a reusable element.

First, bootstrap JSPM by running jspm init. All question can be left with default answers except choosing TypeScript as the transpiler.

Second, instruct SystemJS to assume ts as the default extension when loading your code. I usually place it in the src folder and so update config.js file accordingly by adding the packages property for the sources folder.

config.jslink
1
2
3
4
5
6
7
8
9
System.config({

  packages: {
    "src": {
      "defaultExtension": "ts"
    }
  }

});

Lastly, you will need PolymerTS itself and SystemJS plugin for loading HTML files using the ES6 import syntax. They are installed by running:

1
2
3
bower init
bower i nippur72/PolymerTS --save
jspm i html=github:Hypercubed/systemjs-plugin-html

Note that unlike Juha Järvi, I install systemjs-plugin-html from jspm and not bower. It is also crucial that you explicitly set the name for the plugin by installing with html= prefix. Otherwise bundling, which I explain later in this post, will not work.

Creating elements

Internal dependencies and HTML templates

Because I’m using SystemJS with a transpiler, each element will be split into separate html and ts files. The HTML will contain the <dom-module> element but no script. Instead, each of the elements’ code will import the template using the import syntax via the systemjs-plugin-html plugin. Note the .html! suffix. This is the outline of my <md-ed> element.

md-ed.tslink
1
2
3
4
5
6
7
8
9
import './md-ed.html!'
import {DefaultMdBehavior} from 'DefaultMdBehavior';

@component('md-ed')
@behavior(DefaultMdBehavior)
class MdEd extends polymer.Base {
}

MdEd.register();

Similarly, any shared module or other local elements can be referenced using modules. Above you can see the second line which imports a behavior.

External library dependencies

With the help of JSPM and SystemJS, your elements written in TypeScript (or ES6 I imagine) can reference virtually any external library. They can be packaged as AMD or CommonJS modules or as globals. JSPM unifies the module definitions so that most libraries simply work in the browser.

The example component uses the marked library to parse markdown. It is an npm module which I install with JSPM as usual.

1
jspm i npm:marked

Now, it’s possible to import the library and use its functionality in the custom element:

md-ed.tslink
1
2
3
4
5
6
7
8
9
10
11
12
13
import 'marked';

class MdEd extends polymer.Base {

    @property({ notify: true })
    markdown:String;

    @observe('markdown')
    _markdownChanged(md) {
        var html = marked(md);
        // do something with parsed markdown
    }
}

External web component dependencies

Most web components are currently installed with bower. This is true for Google’s elements from elements.polymer-project.org and most I’ve seen on customelements.io. Bower is used because it creates a flat directory structure which allows for predictable import links. Unfortunately, there is no built-in way for importing such dependencies. Also bundling won’t work for elements which explicitly import polymer.html. There is currently no way to exclude certain imports from the bundle which causes multiple Polymers. Needless to say, it is bad.

So, if you need to reference a third party component like some Iron or Paper Elements simply install them from bower but don’t import them in any of your source files. Instead they will all be imported in an entrypoint - separate for Bower and JSPM.

Publishing for Bower

Follow the instructions below if you want to publish you element to be consumed from Bower.

Bundling

Bundling is done by running the JSPM CLI which has a number of options. For Bower, I’ve found the bundle-sfx command works best, because it allows creating packages which require neither any specific module loader nor JSPM/SystemJS. Elements bundled this way will be possible to consume using bower just like any other element.

I usually add the bundling command to NPM scripts:

package.jsonlink
1
2
3
4
5
{
  "scripts": {
    "build-bower": "jspm bundle-sfx src/md-ed - marked dist/bower/build.js --format global --globals \"{'marked': 'marked'}\""
  }
}

src/md-ed - marked dist/build/build.js means that the root src/md-ed.ts file and it’s dependent modules will be bundled into dist/bower/build.js but will not include the marked library. The marked library will be added later as a bower dependency.

--format global creates a bundle without any module loaders. This is enough for bower and web components.

Finally, the --globals "{'marked': 'marked'}" switch is required for some excluded modules when bundling. It tells JSPM what global variable to use when injecting dependencies into your bundled modules.

I’m intentionally not minifying the contents. The consumer will do so when bundling his or her actual application.

Now, running npm run build-bower will create a bower/dist/build.js with transpiled and bundled scripts and bower/dist/build.html with vulcanized files. Interestingly, the html must exist beforehand, which looks like a bug in the SystemJS html plugin. Simply create one before running the npm script:

1
2
3
mkdir dist
touch build.html
npm run build-bower

Oh, and don’t exclude the dist folder from git. You’ll want to push the bundled files with everything else.

Packaging

Most components published with Bower include a html file named same as the repository (and element). My element is called md-ed and so I created a md-ed.html file in the root of my repository. This will be the main entrypoint for consumers to import. Here’s the complete file:

md-ed.htmllink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- imports of bower dependencies -->
<link rel="import" href="../polymer-ts/polymer-ts.min.html"/>
<link rel="import" href="../paper-input/paper-textarea.html" />
<link rel="import" href="../paper-tabs/paper-tabs.html" />
<link rel="import" href="../iron-pages/iron-pages.html" />
<script src="../marked/lib/marked.js"></script>

<!-- import of bundled HTML files -->
<link rel="import" href="dist/bower/build.html" />

<!-- this is required due to a bug in HTML loader for SystemJS -->
<script>
    var System = System || {};
    System.register = System.register || function(){};
</script>

<!-- referencing the bundled, transpiled code of the element -->
<script src="dist/bower/build.js"></script>

At the top I added bower dependencies. It’s important that the paths don’t include bower_components. On the consumer side, the elements will already live alongside other bower dependencies. I include all component dependencies and marked, which I excluded from the bundle. Shall you choose not to exclude some dependency, you would then keep it out of your bower entrypoint.

Below the bundled files are referenced. There is some additional boilerplate here. The extra script is a remedy for another shortcoming of the systemjs-plugin-html. It doesn’t play nice with the bundle-sfx command and leaves some references to SystemJS. This is simply to avoid System is undefined or similar errors.

Finally, you may also want to add the file to you bower.json as "main": "md-ed.html".

bower.jsonlink
1
2
3
4
{
  "name": "md-ed",
  "main": "md-ed.html"
}

Consuming

Consuming with Bower is as easy as it gets. Simply install the element:

1
bower install --save tpluscode/md-ed

add an import <link> and use the element on you page:

https://github.com/tpluscode/md-ed-sample/blob/bower/index.html
1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
    <script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
    <link rel="import" href="bower_components/md-ed/md-ed.html"/>
</head>
<body>
    <md-ed></md-ed>
</body>
</html>

Publishing for JSPM

Follow the instructions below if you want to publish you element to be consumed from JSPM.

Bundling

Unfortunately, the same bundling command doesn’t work for both Bower and JSPM. I’ve found that for JSPM it is best to use the jspm bundle command which produces a similar output but for use exclusively with SystemJS and no other module loaders. The npm script is similar but simpler than the command used for Bower:

package.jsonlink
1
2
3
4
5
{
  "scripts": {
    "build-jspm": "jspm bundle src/md-ed - marked dist/jspm/bundle.js"
  }
}

It produces a similar output - combined scripts in dist/jspm/bundle.js file and vulcanized dist/jspm/bundle.html. Here the marked library is also excluded from the bundle.

Packaging

For consumers to be able to use your JSPM package it is also necessary to create a main entrypoint. For that purpose I created an md-ed.js file in the root of the repository.

md-ed.jslink
1
2
3
4
5
6
7
8
9
import "bower_components/polymer-ts/polymer-ts.min.html!";
import "bower_components/paper-input/paper-textarea.html!";
import "bower_components/paper-tabs/paper-tabs.html!";
import "bower_components/iron-pages/iron-pages.html!";

import './dist/jspm/bundle.html!'
import './dist/jspm/bundle'

System.import('src/md-ed.ts');

The outline is very similar to Bower’s entrypoint:

  1. Import bower dependencies with HTML plugin
  2. Import the bundled HTML and scripts
  3. Load the element from the bundle

The last step is necessary because JSPM bundles don’t immediately load any modules. They are just used to combine multiple modules in one script.

For the element’s package to be installed correctly, the configuration file must include the main file, similarly to that of bower.

A perceptive reader will also notice that I’m using ES6 module syntax above. SystemJS can handle this just fine provided the format option is set in package.json. Here’s mine, with both entrypoint script and the format set.

package.jsonlink
1
2
3
4
5
6
{
  "jspm": {
    "main": "md-ed.js",
    "format": "es6"
  }
}

Publishing a package in ES6 syntax will also enable rescursive bundling of the element’s dependencies. Otherwise JSPM would not be able to bundle direct usages of System.import. In other words some dependencies would remain unbundled.

Consuming

Consumers, in order to us the element, must install it using JSPM but also install the necessary bower packages. The easiest seems to be installing the same element from both JSPM and bower. This way, albeit cumbersome when updating, will ensure that all necessary dependencies are pulled as well. To install the sample element one would eun the two commands

1
2
bower i tpluscode/md-ed --save
jspm i github:tpluscode/md-ed

Typically there would be single application module, like app.js, which references all it’s dependencies. For our jspm component the import would be a simple import 'tpluscode/md-ed'

At runtime, it will pull all necessary files from bower and jspm components. The main index.html file will then reference the app.js script and uses SystemJS to load the add.

https://github.com/tpluscode/md-ed-sample/blob/jspm/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
        <html>
<head>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
    <script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
</head>
<body>

<md-ed></md-ed>

<script>
    System.import('app');
</script>
</body>
</html>

Conclusion

I realize that the presented ideas are far from ideal. The web stack is not yet consistent enough, with its multiple package managers etc, to support the modern ideas around web components. Until it matures I hope that someone out there will find my ideas helpful.

And please, if you think my bundling routine can be simplified, do leave me a note in the comments.

Comments