Repository navigation
feat: resolve widget bundles' AMD externals generically - #343
Merged
Merged
Conversation
cpsievert
commented
Oct 27, 2026
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
force-pushed
the
generic-amd-externals
branch
from
October 28, 2026 00:32
1165aee to
7887948
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Some widget packages are built with a webpack
externalsconfig, so their AMD bundle declares bare require.js dependencies instead of bundling everything: ipyvolume and pythreejs both externalizethree, jupyter-vuetify externalizesjupyter-vue. In the classic notebook these resolve because every enabled nbextension'sextension.jsruns at page load and contributes to a globalrequire.configregistry (e.g., jupyter-threejs'sextension.jsglobally mapsthree; jupyter-vue's mapsjupyter-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 thedefine([...])header of the widget's nbextension bundle and resolves each external it doesn't already provide:three.jsnext toindex.js): served under an alias scoped to the module via require.jsmap, so two packages shipping incompatible builds under the same name (ipyvolume's and pythreejs'sthree.jsdiffer) each get exactly the build they were bundled with. (The notebook's globalmap: {"*"}is last-write-wins and gets this wrong; scoping avoids the entire class of global dependency conflicts.) UMD siblings get ashimwhose exported global is sniffed from the bundle wrapper.jupyter-vue): mounted and path-configured as their ownHTMLDependency, recursively.To support the second case,
require_dependencies()now returns a list ofHTMLDependencies(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
THREEshim 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: cleanmake test-unit: 437 passed, 1 known xfail (incl. new tests: scoped aliases, UMD shim sniffing, external/transitive/missing nbextension resolution)render_maplibre(7),ipyleaflet_marker_click(1, exercises the/nbextensions/static mount),buffer_transport(1),amd_externals(1) — 10 passedStacked on this PR: #346 (browser-created widget comms), then #324 (pythreejs), then #329 (ipyvolume). Merge order: #343 → #346 → #324 → #329.