Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS error when loading embedded HTML output on Anywidget #369

Closed
kylebarron opened this issue Nov 2, 2023 · 6 comments
Closed

JS error when loading embedded HTML output on Anywidget #369

kylebarron opened this issue Nov 2, 2023 · 6 comments

Comments

@kylebarron
Copy link
Contributor

kylebarron commented Nov 2, 2023

It's not clear that the ultimate issue here is in Anywidget, but I figured it would be ok to start with an issue here. I'm making lonboard as a fast map rendering library and widget. It would be nice to support standalone HTML export and nbconvert conversion to HTML. When I create a minimal map, e.g. with:

import geopandas as gpd
from lonboard import viz
import ipywidgets.embed

gdf = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))

map_ = viz(gdf, radius_min_pixels=10)
map_

ipywidgets.embed.embed_minimal_html('minimal.html', views=[map_], drop_defaults=False)

The map renders fine within Jupyter Notebook:
image

But loading the exported HTML fails with

TypeError: Cannot read properties of undefined (reading 'prototype')
    at enhancePointerEventInput (cd357310-fde6-48b7-a3a9-c8ad15fa6425:65439:41)
    at cd357310-fde6-48b7-a3a9-c8ad15fa6425:65475:1

That's specifically coming from a bundled library that's a dependency of mine (via deckgl).

// node_modules/mjolnir.js/dist/esm/utils/hammer.browser.js
var hammerjs = __toESM(require_hammer());

...

// node_modules/mjolnir.js/dist/esm/utils/hammer.browser.js
enhancePointerEventInput(hammerjs.PointerEventInput);

...

function enhancePointerEventInput(PointerEventInput2) {
  const oldHandler = PointerEventInput2.prototype.handler;
  PointerEventInput2.prototype.handler = function handler(ev) {
    const store = this.store;
    if (ev.button > 0 && ev.type === "pointerdown") {
      if (!some(store, (e) => e.pointerId === ev.pointerId)) {
        store.push(ev);
      }
    }
    oldHandler.call(this, ev);
  };
}

It's weird that this works fine in the notebook but not standalone. My hunch is that there's a difference in how the JS gets run inside the notebook vs as a standalone file.

If we look at what ipywidgets.embed.embed_minimal_html does, it's described here and essentially creates:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>IPyWidget export</title>
</head>
<body>


<!-- Load require.js. Delete this if your page already loads require.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js" integrity="sha256-Ae2Vz/4ePdIu6ZyI/5ZGsYnb+m0JlOmKPjt6XZ9JJkA=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@jupyter-widgets/html-manager@^1.0.1/dist/embed-amd.js" crossorigin="anonymous"></script>

<script type="application/vnd.jupyter.widget-state+json">
{
  "version_major": 2,
  "version_minor": 0,
...

So it uses standard script tags for require.js and @jupyter-widgets/html-manager as well as for the custom widget state. I imagine there must be some difference in the above generated HTML that means the anywidget code isn't getting run as ESM...? (I'm definitely not an expert on this part of JS).

As repro, I'm attaching a zipfile with minimal.ipynb that has the same Python code as above, and minimal.html with the export. This is using lonboard 0.2.0 but with minify set to false so the exported HTML can be somewhat readable.

minimal.zip

@kylebarron
Copy link
Contributor Author

Ok my head is spinning but I somewhat got it to work..?

It's actually a bundling issue because hammerjs is super old and is still a common js module... The bundled code generation (from esbuild) looks like this:

      assign(Hammer, {
        INPUT_START: INPUT_START2,
        INPUT_MOVE: INPUT_MOVE2,
        ...
      });
      var freeGlobal = typeof window3 !== "undefined" ? window3 : typeof self !== "undefined" ? self : {};
      freeGlobal.Hammer = Hammer;
      if (typeof define === "function" && define.amd) {
        define(function() {
          return Hammer;
        });
      } else if (typeof module2 != "undefined" && module2.exports) {
        module2.exports = Hammer;
      } else {
        window3[exportName] = Hammer;
      }
    })(window, document, "Hammer");
  }
});

If we manually change this to:

-      if (typeof define === "function" && define.amd) {
-        define(function() {
-          return Hammer;
-        });
-      } else if (typeof module2 != "undefined" && module2.exports) {
+      if (typeof module2 != "undefined" && module2.exports) {
        module2.exports = Hammer;
      } else {
        window3[exportName] = Hammer;
      }

Then it works. 🤯

(Hammer is always set on window, but not returned via

var hammerjs = __toESM(require_hammer());

)

It seems that it's different outside of a notebook because html-manager is just a different way of executing the code, and so there's some difference between the esm code running natively in jupyterlab vs through html-manager.

image

@manzt
Copy link
Owner

manzt commented Nov 3, 2023

Ah yes, this was my suspicion (some runtime detection of globals). I think you can configure esbuild to eliminate that if block with define.

import esbuild from "esbuild";

esbuild.build({
  entryPoints: [
    "./src/index.tsx",
  ],
  bundle: true,
  minify: true,
  target: ["es2020"],
  outdir: "lonboard/static/",
  format: "esm",
  define: {
    'define.amd': 'false',
  },
});

@kylebarron
Copy link
Contributor Author

🤯 I can't believe it, that just worked. Thanks so much!

@cornhundred
Copy link

Hi @kylebarron and @manzt, I'm having an issue importing deck.gl from esm.sh in certain Jupyter notebook contexts (namely Terra.bio vs Google Colab) and the issue seems to be related to this issue (e.g., hammer.js). Please let me know if you all have any ideas on what might be going wrong.

@kylebarron
Copy link
Contributor Author

I've never tried to use deck from esm.sh

@manzt
Copy link
Owner

manzt commented Apr 3, 2024

I would recommend using esbuild with the configuration I mentioned above. Deck seems like a tricky dependency and passing off bundling to a CDN services is inconsistent, for exactly the same reason Kyle had above: define is sometimes defined and sometimes not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants