Generating plain javascript es6 modules from notebooks

It seems to me that we ought to be able to automatically convert notebooks to vanilla js.

// https://observablehq.com/@pentacular/es6-module-generation-test@16
export default function define(runtime, observer) {
  const main = runtime.module();
  main.variable(observer()).define(["md"], function(md){return( md`# ES6 Module Generation Test`)});
  main.variable(observer("times")).define("times", function(){return( (a, b) => a * b)});
  main.variable(observer()).define(["md","times"], function(md,times){return( md`3 times 4 is ${times(3, 4)}`)});
  main.variable(observer("square")).define("square", ["times"], function(times){return( (x) => times(x, x))});
  main.variable(observer()).define(["md","square"], function(md,square){return( md`The square of 4 is ${square(4)}`)});
  return main;
}

into

const times = (a, b) => a * b;

const square = (x) => times(x, x);

which would allow the definitions in the notebook to be used with vanilla js outside of the observable framework.

This module wouldn’t support redefinition, but that’s not normally used with vanilla js.
You’d also need to do a topological ordering of the definitions by dependency, but that’s not particularly difficult.

md tags could be stripped or rewritten to call some hookable function, and side-effecting leaves could be dropped or gathered into a ‘main’ function of some kind.

I haven’t thought about imports within the module, but since the es6 imports are dynamic, it probably shouldn’t be too hard.

I was considering just writing a converter, but I thought I’d see if there are issues I’ve overlooked, or if this is already planned.

Thanks.

It’s possible if you want to forgo reactivity. Here’s one approach:

2 Likes

Looks about what I expected – import is the hard problem.

I guess there is no plan to support this kind of thing via api.observablehq.com ?

This is going to be a feature in the unofficial compiler very soon! @bgchen has a PR here (that I haven’t reviewed yet, m bad) https://github.com/asg017/unofficial-observablehq-compiler/pull/15

EDIT: Ah wait, after re-reading this, the above won’t 100% do what you’re asking, but it’s still related in the realm of “de-observable-fying” observable notebooks

1 Like

The compiled ES module is vanilla JavaScript; it just depends on our open-source runtime for reactivity. We don’t plan on supporting non-reactive compilation because it would severely limit the functionality of the notebook. Fortunately the runtime is tiny! 10.8 KB gzipped.

If you have feedback or suggestions on making the runtime API more convenient to use, we’d love to hear it.

1 Like

By vanilla I mean without a framework, but sure – I’m not saying that the compiled ES module is not valid javascript. :slight_smile:

Maybe I’m missing something, but what’s the benefit of reactivity when operating in an environment where things will not be redefined?

I guess it serves as a simplified kind of memoization, but other than that?

Looking at the PR, it seems like it reproduces the compilation of a notebook to the same form as api.observablehq.com/@foo/bar.js?v=3 which is very nice.

But, yeah, it doesn’t seem to make the definition cells available for direct import.

Reactivity is frequently used to define inputs (views, user interfaces) and dynamic values (generators, e.g., streaming data or scripted animations).

Sure, but those don’t seem to be things that you’d be using outside of the observablehq framework.

Or maybe I’m overlooking some use-cases here?

One use case is deploying user interfaces developed inside of Observable outside of Observable. Reactivity and other features of the runtime are what power many user interface idioms in Observable notebooks.

Sure, but those would still be within the observable framework, per se.

What do you mean by “within” or “outside” the Observable framework? The idea is that you can build interactive visualizations or general-purpose user interfaces in Observable notebooks (using dataflow) and then embed them in your web application.

1 Like

Right, by embedding the observable framework within the application.

I’m interested in being able to use observable notebooks as a form of literate programming with interactive examples, but with a payload that can be imported as ordinary code.

e.g., for a trivial example

md`Square`
square = (x) => x * x;
viewof n = html`<input type=number>`;
md`The square of ${n} is ${square(n)}.`;

Now we can look at the notebook and read about square, and explore what it does.

Having done that, it would be nice if you could say something like this in ordinary code.

// See http://observablehq.com/@pentacular/square
import { square } from '@pentacular/square';

export const quux = (bar) => foo(square(bar));

And whoever uses quux does not need to consider the observable framework, because it isn’t involved at the point of the call to square.

Which seems like a nice marriage of the static and reactive use-cases.

1 Like

Maybe I’m splitting hairs here, but I think of the runtime as a library rather than a framework, in the sense that it doesn’t dictate anything about how the larger application is built. So you can for instance embed something (reactive) authored in Observable in a React application using our runtime—you don’t have to build our whole application in our “framework”.

But you’re correct that the exposed interface of the embedded notebook is a reactive interface, not a static declaration of values, as necessitated by the generality of reactivity. And we’re not focused on using Observable to author non-reactive, general-purpose code; we are focused on helping people realize better interactive interfaces.

1 Like

That’s fair enough.

Thanks for explaining the situation.

I’ll put together a little transform from the v=3 output for my own use, although it would have been nice to have it as a built-in option. :slight_smile: