Static content for jupyterlab extension

#1

Dear all,
currently I try to find my way into Jupyterlab extension development. What I cannot figure out is how to include static html or image content with an extension.
Let’s say, I want to modify the tutorial https://jupyterlab.readthedocs.io/en/stable/developer/xkcd_extension_tutorial.html to show a static image provided with my extension:

class XkcdWidget extends Widget {
  constructor() {
    ...
    this.img = document.createElement('img');
    this.img.className = 'jp-xkcdCartoon';
    this.img.src='/path/to/static/img.png';
    this.node.appendChild(this.img);

Where do I have to put /path/to/static/img.png relative to my extension? How does Jupyterlab know about this static path?

If I save the image at $CONDA_PREFIX/share/jupyter/lab/static/img.png, it is accessible, but I want to include it in my extension.

Thank you
Ernst

Loading static CSS in JupyterLab
#2

This is honestly pretty tough, since as extension developers we have no control over the Webpack config.

Here are a few strategies I’ve used with success:

Pure CSS

Write a CSS file, targeting your .jp-xkcdCartoon, and have it reference the image by URL. Eg,

/* style.css */
.jp-xkcdCartoon {
  backgroundImage: url('./local/path/to/img.png');
}

And in your JS file, import the CSS file:

// main.ts
import "./path/to/style.css"

This works because Webpack will see the “import” in your JS file, and follow it. Then, a separate Webpack plugin (defined in the JupyterLab build config) will in-line the stylesheet and the image. This is the most reliable, but rather inflexible and means you won’t be able to get the source data from JS.

URL loading

Using url-loader is best if you need access to raw data from within JS.

// main.ts
import * as comic from "./path/to/img.png"
// `comic` will be a Base64 string

Note that for some file types, this may not work directly. You can force the use of url-loader with a prefix:

// main.ts
import * as myTemplateFile from "url-loader!./path/to/my/data.html";
// myTemplateFile will be a regular text string

File reference

If none of those work, you can use file-loader to import it. This will make Webpack include it with the build, but won’t give you direct data access (you’ll need to fetch() it yourself). I’ve found this to be the least reliable method, because you need to be very careful constructing URLs (making sure they work on end-user deployments), but it has the benefit of not needing to pass data around directly.

Wrapping it all up

The build system in JupyterLab is very complicated right now, unfortunately. This is by necessity, given the technical constraints imposed by the build-chain (and the web as a whole). You can follow discussion on future direction here: Notes on JupyterLab Build

2 Likes
#3

Your css solution looks good for images. But the image was just an example. I need to include any static content for different purposes, be it html, javascript, pdf, docx or whatever files. One purpose, among others, is showing content (authored independent from jupyterlab) in an iframe that should access these files from the extension.

I’ll try if url-loader or file-loader work for me. I never saw a fetch() needed with file-loader. Is this a jupyterlab thing? Can you point me to a usage example?

You mention that we have no control over the webpack config. I find it very confusing that jlpm wraps the whole build process, and I have no idea what is going on under the hood, where webpack.config.js is located etc. Are these wrappers documented somewhere (technically, not from the user point of view) beyond https://jupyterlab.readthedocs.io/en/stable/developer/extension_dev.html#extension-authoring? This might enable developers to get a much better understanding.

Thank you very much
Ernst

#4

In general, the build system of lab is best treated as an implementation detail. As there are already quite a few pain points with it, it is likely to change somehow in the future. Secondly, Given your requirement of serving generic files, are you sure you want to build these files into the jupyterlab bundle as a static asset? It might be better to add a server extension to serve up your files as a separate REST api.

#5

How do I add a server extension to serve up my files as a separate REST api? I don’t want to spin up another server, but if I can write a jupyterlab extension that lets it serve my static files, this would probably answer my question.

And as to the undocumented build system, I’d rather solve problems for the current implementation and adapt to future changes than not being able to do anything until jupyterlab has consolidated. But I get your point.

Thank you
Ernst

#6

Sorry, I think I had a wire crossed on the file-loader w/ fetch(). It’s not required, I was actually thinking of another loader that copies the file and exposes a partial URL to that file. File-loader and url-loader should both work for static content.

As Vidar mentioned, if you have a more generic need for serving these files (ie, these files may change), a Jupyter ServerExtension might work best for you. Here’s a short tutorial on an API handler: https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.html. Server extensions use the Tornado library internally. Note that you need a separate install step for them: (jupyter serverextension enable --py your.python.module). Once enabled, it’ll spin up automatically when Jupyter is started.

An unrelated canard about iframes: A lot of things will break inside them. Eg, if you have an iframe inside a notebook, and had it focused, you would not be able to run any notebook keyboard shortcuts (like 0, 0 to restart the kernel).