26 March 2019

Note
This post has been updated to also include Pika

When prototyping a Javascript-based web-application, I prefer a lightweight approach in which I just have VSCode, the latest Chrome version and browser-sync. No transpiler, bundler, etc. The browser is refreshed each time I save a file and I get immediate feedback on any CSS, HTML or JavaScript changes I have made.

Unfortunately, just using browser-sync does not work as soon as you want to import ES6 modules from third-party. Like, for example, lit-element.

I will show in what cases ES6 imports are not working natively in the browser for external dependencies and show different mechanism to work around it for your development environment.

Problem

An ES6 import will cause problems as soon as you have bare imports. A bare import is one that you usually see when working with bundlers like Webpack: it is not a relative path to your node_modules but…​ bare.

import { html, LitElement } from 'lit-element/lit-element.js';

And when bundling the application with e.g. Webpack, this would be working fine. But if directly run in the browser, you would see:

2EJ5gzy
Uncaught TypeError: Failed to resolve module specifier "lit-element/lit-element.js". Relative references must start with either "/", "./", or "../".

NodeJS supports bare imports and its resolution but browsers do not support it as of now.

Now I can try to be smart and change it to a relative import

import { html, LitElement } from './lit-element/lit-element.js';

and make browser-sync serve files from the node_modules directory as follows:

browser-sync src node_modules -f src --cors --no-notify

I will get a different but similar error.

Uncaught TypeError: Failed to resolve module specifier "lit-html". Relative references must start with either "/", "./", or "../".

Even though I was no able to import lit-element, it is now choking on lit-html which is a bare import in the lit-element sources itself. So, it seems we are stuck as any external library that contains ES6 imports will fail if the imports are not first rewritten like Webpack will do.

Solutions

Here are the solutions I have found when my main requirement is to keep a good developer experience like I have with browser-sync alone (lean and simple).

Unpkg.com

Unpkg acts like a CDN and offers popular NPM packages via http. The nice thing is that bare imports are rewritten. So, changing the import to this will work fine:

import { html, LitElement } from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';

The ?module does the magic of rewriting bare imports.

I can now continue working with browser-sync like before:

browser-sync src -f src --cors --no-notify

The downside of this approach is that the application is not local/self-contained; I have to fetch something from the internet; which can be bad if your internet speed is slow. Actually, it will be cached; but it will hit the internet anyway for cache-validation. Also, this will not work if you are trying to work offline.

Webpack

As mentioned before, a bundler sloves the import problem for us by inlining or rewriting the imports. But I am no fan of this approach as this bundling step can slow down the turn-around time from saving the file to the browser actually reloading. Anyway, the steps are:

  1. `npm install --save-dev webpack webpack-cli copy-webpack-plugin webpack-dev-server `

  2. Create webpack.config.js:

    const path = require('path');
    const CopyPlugin = require('copy-webpack-plugin');
    
    module.exports = {
        entry: './src/app.js',
        mode: 'development',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'app.js'
        },
        devServer: {
            contentBase: './dist'
        },
        plugins: [
            new CopyPlugin([
                { from: 'src/index.html', to: './' },
                { from: 'src/style.css', to: './' },
            ]),
        ],
    };
  3. Add a script to the package.json: "dev": "webpack-dev-server --open"

  4. The import can now look like this:

    import { html, LitElement } from 'lit-element/lit-element.js';

Run the dev-server with live-reload (similar to browser-sync) with npm run dev.

After trying it for a small application and really only doing the bare minimum with Webpack, I have to say it is a viable option. But it requires to download some dependencies from NPM and create a webpack.config.js.

Open Web Components (OWC)

Open Web Components offer a simple dev-server that does nothing more than rewrite the bar module imports to relative imports.

npm install --save-dev owc-dev-server

After trying it out, I was disappointed to find that the dev-server does not offer live-reloading.

The best solution I found was to combine it with browser-sync. Here are the scripts I added to my package.json

"dev": "owc-dev-server | npm run watch",
"watch": "browser-sync start -f src/ --proxy localhost:8080 --startPath src",

Note that watch is just a helper-script used by dev; so you have to use npm run dev.

Polymer-cli

The last tool I tried was Polymer-CLI. In the end, the approach is a mix between the previous two. It requires an additional polymer.json config-file and it also does not function without browser-sync.

The steps are:

  1. npm install --save-dev polymer-cli

  2. Create polymer.json:

    {
        "entrypoint": "src/index.html",
        "shell": "src/app.js",
        "npm": true
    }
  3. Set up scripts:

    "watch": "browser-sync start -f src/ --proxy localhost:8000 --startPath src",
    "dev": "polymer serve --open-path src/index.html | npm run watch"

See here for the issue to natively support live-reload.

Pika

One more nice tool was mentioned to me in the reactions to this post. So, I felt inclined to try it and after all also include it here.

What @pika/web does, is described nicely in this article. It actually is a great addition to my post because it adds to the same discussion that you should not be required to use bundlers just to get all the webcomponents / ES6 goodness working.

Pika moves the bundling step from where you have to run the bundler for your application, to just running a bundler/tool once for each installed dependency in your package.json. I.e. what it does is take your dependencies from node_modules and repackages/bundles them under the folder web_modules. The repackaged dependency no longer contains bare imports and can easily be include. Just run

npm install && npx @pika/web

Now, you could import like below and continue using browser-sync.

import { html, LitElement } from './web_modules/lit-element.js';

Note that I don’t like having to put web_modules in the path. So what I ended up doing was importing like this

import { html, LitElement } from './lit-element.js';

and just let browser-sync serve from src and web_modules.

browser-sync src web_modules -f src --cors --no-notify

Summary

After trying out all these options, I have to say that non is as lightweight and simple as using plain browser-sync.

I can work with the Webpack and the OCW approaches. Webpack is a standard tool to learn anyway. And OCW has a lightweight dev-serverthat just rewrites the imports on the fly; no bundling step. But sadly, it does not come with live-reload out of the box and requries to combine it with browser-sync. Polymer-CLI is just to heavyweight for what I need from it (also requiring a config-file) and unpkg.com is no option as I want to be able to work offline.

Pika was only added after I intially wrote this post. But I will keep trying it in the next way. From the first impression, I have to say that I really like that I can just continue using plain browser-sync.

As the dependency on other libraries via ES6 imports will only get more important, I am eagerly awaiting a solution. Maybe import-maps will the way to go.