Search code examples
ioswebpackbabeljsweb-componentwebpack-2

How to use CustomElement v1 polyfill in a webpack/babel build?


I'm having some trouble getting this WebComponents polyfill + native-shim to work right across all devices, though webpack.

Some background on my setup: * Webpack2 + babel-6 * app is written in ES6, transpiling to ES5 * imports a node_module package written in ES6, which defines/registers a CustomElement used in the app

So the relevant webpack dev config looks something like this:

const config = webpackMerge(baseConfig, {
  entry: [
    'webpack/hot/only-dev-server',
    '@webcomponents/custom-elements/src/native-shim',
    '@webcomponents/custom-elements',
    '<module that uses CustomElements>/dist/src/main',
    './src/client',
  ],
  output: {
    path: path.resolve(__dirname, './../dist/assets/'),
    filename: 'app.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
        },
        include: [
          path.join(NODE_MODULES_DIR, '<module that uses CustomElements>'),
          path.join(__dirname, '../src'),
        ],
      },
    ],
  },
...

key take aways: * I need CustomElement poly loaded before <module that uses CustomElements> * I need <module that uses CustomElements> loaded before my app soure * <module that uses CustomElements> is ES6 so we're transpiling it ( thus the include in the babel-loader).

The above works as-expected in modern ES6 browsers ( IE desktop Chrome ), HOWEVER

it does not work in older browsers. I get the following error in older browsers, for example iOS 8:

SyntaxError: Unexpected token ')'

pointing to the opening anonymous function in the native-shim pollyfill:

(() => {
  'use strict';

  // Do nothing if `customElements` does not exist.
  if (!window.customElements) return;

  const NativeHTMLElement = window.HTMLElement;
  const nativeDefine = window.customElements.define;
  const nativeGet = window.customElements.get;

So it seems to me like the native-shim would need to be transpiled to ES5:

        include: [
+         path.join(NODE_MODULES_DIR, '@webcomponents/custom-elements/src/native-shim'),
          path.join(NODE_MODULES_DIR, '<module that uses CustomElements>'),
          path.join(__dirname, '../src'),
        ],

...but doing so now breaks both Chrome and iOS 8 with the following error:

app.js:1 Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
    at new StandInElement (native-shim.js:122)
    at HTMLDocument.createElement (<anonymous>:1:1545)
    at ReactDOMComponent.mountComponent (ReactDOMComponent.js:504)
    at Object.mountComponent (ReactReconciler.js:46)
    at ReactCompositeComponentWrapper.performInitialMount (ReactCompositeComponent.js:371)
    at ReactCompositeComponentWrapper.mountComponent (ReactCompositeComponent.js:258)
    at Object.mountComponent (ReactReconciler.js:46)
    at Object.updateChildren (ReactChildReconciler.js:121)
    at ReactDOMComponent._reconcilerUpdateChildren (ReactMultiChild.js:208)
    at ReactDOMComponent._updateChildren (ReactMultiChild.js:312)

.. which takes me to this constructor() line in the native-shim:

  window.customElements.define = (tagname, elementClass) => {
    const elementProto = elementClass.prototype;
    const StandInElement = class extends NativeHTMLElement {
      constructor() {

Phew. So it's very unclear to me how we actually include this in a webpack based build, where the dependency using CustomElements is ES6 ( and needs transpiling).

  • Transpiling the native-shim to es5 doesn't work
  • using the native-shim as-is at the top of the bundle entry point doesn't work for iOS 8, but does for Chrome
  • not including the native-shim breaks both Chrome and iOS

I'm really quite frustrated with web components at this point. I just want to use this one dependency that happens to be built with web components. How can I get it to work properly in a webpack build, and work across all devices? Am I missing something obvious here?

My .babelrc config for posterity sake (dev config most relevant):

{
  "presets": [
    ["es2015", { "modules": false }],
    "react"
  ],
  "plugins": [
    "transform-custom-element-classes",
    "transform-object-rest-spread",
    "transform-object-assign",
    "transform-exponentiation-operator"
  ],
  "env": {
    "test": {
      "plugins": [
        [ "babel-plugin-webpack-alias", { "config": "./cfg/test.js" } ]
      ]
    },
    "dev": {
      "plugins": [
        "react-hot-loader/babel",
        [ "babel-plugin-webpack-alias", { "config": "./cfg/dev.js" } ]
      ]
    },
    "dist": {
      "plugins": [
        [ "babel-plugin-webpack-alias", { "config": "./cfg/dist.js" } ],
        "transform-react-constant-elements",
        "transform-react-remove-prop-types",
        "minify-dead-code-elimination",
        "minify-constant-folding"
      ]
    },
    "production": {
      "plugins": [
        [ "babel-plugin-webpack-alias", { "config": "./cfg/server.js" } ],
        "transform-react-constant-elements",
        "transform-react-remove-prop-types",
        "minify-dead-code-elimination",
        "minify-constant-folding"
      ]
    }
  }
}

Solution

  • I was able to achieve something similar with the .babelrc plugin pipeline below. It looks like the only differences are https://babeljs.io/docs/plugins/transform-es2015-classes/ and https://babeljs.io/docs/plugins/transform-es2015-classes/, but I honestly can't remember what problems those were solving specifically:

    {
      "plugins": [
        "transform-runtime",
        ["babel-plugin-transform-builtin-extend", {
          "globals": ["Error", "Array"]
        }],
        "syntax-async-functions",
        "transform-async-to-generator",
        "transform-custom-element-classes",
        "transform-es2015-classes"
      ]
    }