Skip to content

feat: resolve widget bundles' AMD externals generically - #343

Merged
cpsievert merged 4 commits into
mainfrom
generic-amd-externals
Oct 28, 2026
Merged

cpsievert merged 4 commits into
mainfrom
generic-amd-externals

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Some widget packages are built with a webpack externals config, so their AMD bundle declares bare require.js dependencies instead of bundling everything: ipyvolume and pythreejs both externalize three, jupyter-vuetify externalizes jupyter-vue. In the classic notebook these resolve because every enabled nbextension's extension.js runs at page load and contributes to a global require.config registry (e.g., jupyter-threejs's extension.js globally maps three; jupyter-vue's maps jupyter-vue).

shinywidgets deliberately loads widget modules on demand instead, configuring a path only for the widget's own module — so these externals resolve relative to the app root, 404, and the widget fails to render (or silently falls back to a CDN). This came up concretely in #329 (ipyvolume), which works around it with a hardcoded per-package table; this PR replaces that approach with a generic mechanism so any such widget works without per-package wiring.

What it does

require_dependencies() now parses the define([...]) header of the widget's nbextension bundle and resolves each external it doesn't already provide:

  • Sibling-file externals (e.g., three.js next to index.js): served under an alias scoped to the module via require.js map, so two packages shipping incompatible builds under the same name (ipyvolume's and pythreejs's three.js differ) each get exactly the build they were bundled with. (The notebook's global map: {"*"} is last-write-wins and gets this wrong; scoping avoids the entire class of global dependency conflicts.) UMD siblings get a shim whose exported global is sniffed from the bundle wrapper.
  • Externals that are themselves installed nbextensions (e.g., jupyter-vue): mounted and path-configured as their own HTMLDependency, recursively.

To support the second case, require_dependencies() now returns a list of HTMLDependencies (widget's own module first), and the /nbextensions/ static mount covers each.

Verified against the real ipyvolume/pythreejs/ipyvuetify packages: the generated require.js configs match what #329 produces with its hand-written table (including the sniffed THREE shim for pythreejs's UMD build).

A survey of the widget packages shinywidgets currently supports (bqplot, ipycanvas, ipydatagrid, ipysigma, jupyter_bokeh, jupyter-cytoscape, jupyter-leaflet, jupyter-matplotlib, keplergl, pydeck, ...) found no other bare externals — everything else bundles its dependencies — so this is a no-op for all currently supported widgets and only activates for the webpack-externals generation (ipyvolume, pythreejs, ipyvuetify, ...).

Verification

  • make pyright, make check-format: clean
  • make test-unit: 437 passed, 1 known xfail (incl. new tests: scoped aliases, UMD shim sniffing, external/transitive/missing nbextension resolution)
  • Playwright smoke on this branch: render_maplibre (7), ipyleaflet_marker_click (1, exercises the /nbextensions/ static mount), buffer_transport (1), amd_externals (1) — 10 passed
  • The same mechanism stacked under feat: add render_ipyvolume() for ipyvolume 3D plots #329 passes its 8 ipyvolume Playwright tests (real WebGL rendering) with the per-package table removed

Stacked on this PR: #346 (browser-created widget comms), then #324 (pythreejs), then #329 (ipyvolume). Merge order: #343 → #346 → #324 → #329.

Comment thread shinywidgets/_dependencies.py Outdated
Widget packages built with a webpack externals config (e.g., ipyvolume
and pythreejs externalize three; jupyter-vuetify externalizes jupyter-vue)
rely on the classic notebook's global require.config registry -- built up
at page load from every enabled nbextension's extension.js -- to resolve
those names. shinywidgets loads widget modules on demand instead, so such
externals resolve relative to the app root (404) and the widget fails to
render (or silently falls back to a CDN).

require_dependency() now parses the define([...]) header of the widget's
nbextension bundle and resolves each external it doesn't already provide:

- An external shipped as a sibling file in the nbextension directory is
  served under an alias scoped to the module via require.js map, so two
  packages shipping incompatible builds under the same name (ipyvolume's
  and pythreejs's three.js) each get their own copy. UMD siblings get a
  shim with the exported global, sniffed from the bundle wrapper (with
  _AMD_UMD_GLOBAL_OVERRIDES as an escape hatch).
- An external naming another installed nbextension is mounted and
  path-configured as its own HTMLDependency, recursively.

To support the second case, require_dependency() now returns a list of
HTMLDependencies (the widget's own module first).
Commit f84590b inadvertently dropped the override table for UMD bundles
whose exported global can't be sniffed from the wrapper. Restore it
(under the new public naming) along with its test.
The override removal is intentional: all known supported bundles work with inferred globals. Use the plural function name to match its list return value.
@cpsievert
cpsievert force-pushed the generic-amd-externals branch from 1165aee to 7887948 Compare October 28, 2026 00:32
@cpsievert
cpsievert merged commit bf22214 into main Oct 28, 2026
5 checks passed
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

Successfully merging this pull request may close these issues.

1 participant