Repository navigation
feat: support precompiled ES modules as template implementations - #106
Merged
Merged
Conversation
maartenbreddels
force-pushed
the
feat/esm-modules
branch
3 times, most recently
from
July 4, 2026 13:37
1ae5834 to
dbe51d9
Compare
maartenbreddels
force-pushed
the
feat/esm-modules
branch
from
July 4, 2026 22:39
d4dfc48 to
6f8a223
Compare
Collaborator
Author
|
API update on the branch: |
maartenbreddels
force-pushed
the
feat/esm-modules
branch
from
July 5, 2026 10:04
af7faf0 to
676a37c
Compare
define_module(name, code_or_path) ships an ES module once per kernel
(imported via the existing es-module-shims machinery and registered in
the import map), usable in two forms:
- Template(esm_module=, esm_export=): the export replaces the in-browser
compiled template. Its options ride as mixins[0] under the ipyvue model
mixin, the same precedence compileSfc produces, so model traits
override script data() defaults and injected event handlers override
methods - existing templates work AOT-compiled without changes.
- {"esm_module": ..., "esm_export": ...} entries in a template's
components dict: the export is used as a tag with its own props/emits,
no mixin.
This allows building .vue files ahead of time (e.g. with vite, vue
external) with npm dependencies compiled in: no template source over the
wire and no vue/compiler-sfc at runtime for these components.
The vue import-map entry is re-added before each module import and
expose() no longer deletes its window global: another library (ipyreact)
can replace the importShim global with its own es-module-shims copy
after our init, and the vue blob may then be evaluated more than once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A module whose default export is a plain vue plugin ({ install(app) })
is app.use'd on every app, current and future. This is the vue3-idiomatic
way for a precompiled bundle to register components globally (vue3 has no
global registry): the names live in the bundle next to the components,
no Python-side registration calls, and the bundle stays a normal vue
plugin usable outside ipyvue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
define_module(name, "/static/public/bundle.mjs") imports the module from the url instead of shipping the code over the widget model - e.g. a bundle served from the app's static dir in production. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ource A plain str was ambiguously code-or-url based on a prefix heuristic; now str always means a url, Path means a file, and inline source moves to an explicit code keyword. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shim must be a page-wide singleton; the script tag is the cross-library mutex. When another library (e.g. ipyreact) is loading its copy, wait for the importShim global instead of proceeding without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ipyvuetify test wheel pins a released ipyvue, downgrading the wheel under test (and losing the modules under test with it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There is one es-module-shims per page, shared with any other library that loads it (ipyreact), and it reads this global once. Overwriting dropped ipyreact's mapOverrides, so re-pointing an import map entry was rejected and its module hot reload silently kept serving the old module. We need mapOverrides for our own redefinitions too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maartenbreddels
force-pushed
the
feat/esm-modules
branch
from
October 5, 2026 08:22
8d1f7d6 to
c805c09
Compare
Closed Module widgets should not keep later modules waiting on names that no longer have a frontend provider. define_module now matches the ipyreact source interface, registers only created widgets, and lets callers override dependencies explicitly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A hot reload could delete the pending module resolver and leave components waiting forever. The registry now keeps pending waiters, replacement loads invalidate synchronously, and stale generations stop before updating import maps, plugins, or consumers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mounted Vue templates did not always rerender when ES modules or ESM template selectors changed after the first render. This change triggers the existing template-change path for late plugins, module replacements, and esm_module/esm_export updates, and keeps only the newest plugin object per module name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ipywidgets_runner tests changed widgets after the kernel code returned, outside the kernel context, so the changes never reached the page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A root $forceUpdate re-ran the root render, but the mounted ESM template was not replaced, so module, export and plugin changes never showed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Redefining a name created a second Module widget whose default dependencies formed a cycle with the other redefined modules. Update the live widget instead, as solara does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Template refreshes could miss nested Vue 3 parents and stale ESM component registrations. This records rendering owners, refreshes module consumers after good modules are provided, and covers late plugin and dependency retry paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nested VueModel parents reused cached child vnodes after a template version bump, so ESM updates could refresh the owner without re-rendering the template child. The cache now includes the template render key, and owner refreshes use Vue's public force-update path without an extra root refresh. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Parent updates could make the wrapper build a new component object and remount the child. This caches the inner component until the template refresh key changes, while forwarding slots, attrs, listeners, and refs through the wrapper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plugin registration now refreshes only the instances that rendered the newly registered tags, and template refresh versions no longer depend on mounted component lifetime. This keeps local widget state intact while still refreshing hidden, root, embedded, and jupyter-widget copies after ESM changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The per-instance refresh machinery grew one review finding at a time and was hard to follow. Triggering change:template on the affected templates reuses the root re-render that template hot reload already relies on, and covers module reloads, ESM selector changes and late plugins with one mechanism. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dule worked A template whose module failed or was still loading had no refresh listener, so fixing the module or changing esm_export left it blank or on the old export. Html(tag=...) widgets and string components in `components` that use a plugin tag also did not re-render once the plugin loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot form a cycle Redefining a module whose widget was closed created a new widget with every later module as dependency, so a->[b] and b->[a] both waited forever after a page reload. Solara avoids this by keeping the first dependency list of a name; do the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Lets a VueTemplate use a precompiled ES module export as its implementation, so
.vuefiles can be built ahead of time instead of compiled in the browser.Problem
Today every ipyvue template ships its
.vuesource to the browser, and the browser compiles it at runtime withvue/compiler-sfc.Authors cannot use npm dependencies in a template without window-global hacks.
Template errors only show up when a user opens the page.
ipyreact already solves this for React with
define_module; ipyvue (and Solara apps on Vue 3) had no equivalent.Change
ipyvue.define_module(name, module: Path=None, *, code=None, url=None, dependencies=None)ships an ES module once per kernel through es-module-shims and the import map.It has the same interface and default as ipyreact: without
dependencies=, a module waits for every module defined before it whose widget is still open.Defining a name again updates its existing widget and keeps the name's first dependency list, the way Solara's
esm_vue.pydoes.Template(esm_module=..., esm_export=...)uses one export as the template's implementation.The export's options are
mixins[0]under the ipyvue model mixin, so Python traits overridedata()and Python event handlers override methods, the same precedence as compiled templates.components={"x": {"esm_module": ..., "esm_export": ...}}uses an export as a tag with its own props and events.{ install(app) }) is installed on every app, current and future.change:templatehot-reload path when a module is defined again, whenesm_module,esm_export,componentsoreventschange, when a module that failed to load is fixed, and when a plugin loads after its tags already rendered (string templates,componentsstrings, ESM templates andHtml(tag=...)).Validation
tests/ui/test_esm_module.pyand 6 unit tests intests/unit/test_esm.py.resolveComponent,Htmltag), unrelated input text kept across a plugin load, module reload at the root, inside an ipyvuetify container, inside another template, and for a root plus embedded copy,components/esm_exportchanges, a failed import that is fixed later, dependency-only recovery, and a reload while a consumer waits.Gaps
data,methodsandcsstraits are not applied to an ESM template.v-ifduring a module reload shows the old export when it is shown again.VueComponent, is not refreshed after a late plugin load.<script setup>exports do not see Python traits.Align results
Caution
/alignwas not run on this change: the design questions were settled with the maintainer in the session instead. They chose ipyreact parity fordefine_module(same interface and default, plus an explicitdependencies=), fixing the review findings in impact order, and then a simplification pass with Opus workers and astra and sol reviewers.Crossreview results
define_modulemade every module depend on every name defined earlier in the Python process, so a second session or test deadlocked (a waits for b, b waits for a). This also made CI red. Fixed: the default skips closed modules, a redefinition updates the live widget, and a name keeps its first dependency list.🤖 Generated with Claude Code