manzt / anywidget

reusable widgets made easy
https://anywidget.dev
MIT License
488 stars 38 forks source link

Add ability to have multiple components in one .js file #375

Closed billti closed 11 months ago

billti commented 11 months ago

Love the project. Thanks for building it!

I have a number of components that will share code, but I still want to ideally ship one .js file containing my components (and their shared code). Being that anywidget expects the Python class for a Widget to contain something like _esm = pathlib.Path("index.js") and for that file to export a render function, that doesn't seem possible however.

Would it be possible to be able configure the render function to use from the esm module, e.g. _esm_renderer = "render_Foo" for example. (Or something more elegant). That way I could have multiple components use the same .js file but use a different renderer function for each.

rgbkrk commented 11 months ago

Since you're approaching a stage of wanting to have a component library to work with, I personally would go with creating a shared ES Module. I recommend trying out esbuild as you can create an ESM bundle that can be reused by other ESM.

esbuild main.tsx --bundle --outdir=dist --format=esm
Example files **`component.tsx`** ```typescript import React from "https://esm.sh/react@18.2.0"; export default function App() { return ( <>

Hi

My name is appster

) } ``` **`main.tsx`** ```typescript import React from "https://esm.sh/react@18.2.0" import Component from "./component.tsx" export function render() { return } ``` ``` $ esbuild main.tsx --bundle --outdir=dist --format=esm ``` **`dist/main.js`** ```javascript // main.tsx import React2 from "https://esm.sh/react@18.2.0"; // component.tsx import React from "https://esm.sh/react@18.2.0"; function App() { return /* @__PURE__ */ React.createElement( React.Fragment, null, /* @__PURE__ */ React.createElement("h1", null, "Hi"), /* @__PURE__ */ React.createElement("p", null, "My name is appster"), ); } // main.tsx function render() { return /* @__PURE__ */ React2.createElement(App, null); } export { render }; ```
billti commented 11 months ago

I am using esbuild and doing exactly what you describe. But how would I expose two different 'render' functions to be used by two different anywidget Python classes from that bundle? You can't, because the exported function is required to be called 'render', so a bundle can only export one function called 'render'.

manzt commented 11 months ago

Thanks for your suggestion on enhancing anywidget's API to allow dynamic determination of renderer function names. While this is an interesting idea, currently anywidget operates with a "one widget per file" approach for simplicity and clarity.

For your use case, I recommend continuing to use esbuild and organizing your code by bundling separate entry points for each widget, while keeping the shared logic in a separate ES module. This way, each widget can import and use the shared functionality as needed:

esbuild --bundle --outdir=dist a-widget.js b-widget.js 
// shared.js
export function sharedFunction() { /* Shared logic */ }

// a-widget.js
import { sharedFunction } from "./shared.js";
export function render({ model, el }) { /* Widget A specific logic */ }

// b-widget.js
import { sharedFunction } from "./shared.js";
export function render({ model, el }) { /* Widget B specific logic */ }
billti commented 11 months ago

Thanks. That's what I'm doing now. Guess I'll continue down that path.

For reference, the motivation is partly due to VS Code and how convoluted loading JS files for widgets is there (https://github.com/microsoft/vscode-jupyter/wiki/Component:-IPyWidgets#loading-3rd-party-source), so just the one fetch for my collection of widgets would have been preferred, but that's not a blocker, and not your issue to solve :-)

manzt commented 11 months ago

Ah ok I see, great to know the use case! Thanks for understanding.

Unfortunately a current hard limitation of anywidget is that a widget entry-point cannot use relative imports. This means that widgets must be bundled separately, even if they share dependencies. With full ESM support, in theory the above could work without a bundler at all (and there would only be one browser request for shared.js). I've been thinking for some time about whether we could support writing fully ESM natively, but have yet to come up with a solution due to the differences in where the ESM is served (i.e., in Jupyter vs JupyterLab vs Colab).