observablehq / framework

A static site generator for data apps, dashboards, reports, and more. Observable Framework combines JavaScript on the front-end for interactive graphics with any language on the back-end for data analysis.
https://observablehq.com/framework/
ISC License
2.38k stars 104 forks source link

A JavaScript API for embedded use cases #1084

Closed mbostock closed 4 days ago

mbostock commented 6 months ago

We currently offer only a command-line interface for previewing, building, and deploying projects. We could offer a JavaScript API that provides more flexibility to transpile Markdown and JavaScript.

kziemski commented 3 weeks ago

@mbostock, this would be a powerful addition and honestly it should be the default for framework in that a observable framework project would work really great if in the barest example it acted as es package module in a monorepo, exporting its pages like components. and that the static site generator was then the layer on top of it that . love the product but i view it as the analytical engine that could go really well in react webapp, potentially expo, refine, storybook and module federation under webpack/rspack.

mbostock commented 2 weeks ago

Current sketch is that you could have a component like this in src/chart.js:

import * as Plot from "npm:@observablehq/plot";
import {FileAttachment} from "npm:@observablehq/stdlib";

export async function MyChart({color} = {}) {
  const gistemp = await FileAttachment("gistemp.csv").csv({typed: true});
  return Plot.plot({marks: [Plot.dot(gistemp, {x: "Date", y: "Anomaly", stroke: color})]});
}

To develop this in Framework (in a similar vein to Storybook), you’d do something like src/chart.md:

```js
import {MyChart} from "./chart.js";

${await MyChart()}


To use this component in another web application, along with its baked data typically generated by a data loader, you’d first (somehow) declare that you want it to be embeddable, resulting in a wrapper script like this generated during build in `dist/chart.js` (or maybe `dist/_embed/chart.js`, TBD):

```js
import {registerFile} from "./_observablehq/stdlib.6bf0b933.js";
import "./_npm/@observablehq/plot@0.6.16/e828d8c8.js"; // module preload

registerFile(new URL("./gistemp.csv", import.meta.url).href, {"name":"./gistemp.csv","mimeType":"text/csv","path":new URL("../_file/gistemp.08e51068.csv", import.meta.url).href,"lastModified":1713890568661});

export * from "./_import/chart.b97f5c97.js";

And like normal, the compiled version of the component in dist/_import/chart.b97f5c97.js:

import * as Plot from "../_npm/@observablehq/plot@0.6.16/e828d8c8.js";
import {FileAttachment} from "../_observablehq/stdlib.6bf0b933.js";

export async function MyChart({color} = {}) {
  const gistemp = await FileAttachment("../gistemp.csv", import.meta.url).csv({typed: true});
  return Plot.plot({marks: [Plot.dot(gistemp, {x: "Date", y: "Anomaly", stroke: color})]});
}

Then, in your vanilla web app, you can do something like:

<script type="module">

import {MyChart} from "https://example.observablehq.cloud/app/chart.js";

document.body.append(await MyChart({color: "red"}));

</script>

Notably, I’m not thinking of pages (or chunks of content within pages) as being embeddable, but instead focusing on embedding of JavaScript components.

kziemski commented 2 weeks ago

if the project compiles to the dist one of things that could be done is package exports. drop a package.json in the dist https://nodejs.org/api/packages.html#package-entry-points with the exports object built out with all of the sub paths.