Skip to content

feat: kernel-aware ES module support for ipyvue - #1169

Merged
maartenbreddels merged 7 commits into
masterfrom
feat/ipyvue-esm-modules
Jul 5, 2026
Merged

feat: kernel-aware ES module support for ipyvue#1169
maartenbreddels merged 7 commits into
masterfrom
feat/ipyvue-esm-modules

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Companion to widgetti/ipyvue#106 (ipyvue's vue3 branch gains define_module for precompiled ESM bundles). This gives those modules the same server-side handling ipyreact modules get in solara/server/esm.py:

  • definitions recorded once (module level), Module widgets materialized per kernel inside the kernel context
  • widgets recreated when context.restart() closed them, so in-place hot reload works (same fix as fix: make ES module (define_module) hot reload survive in-place app reload #1168)
  • define_module(Path) registered with the reloader — rebuilding a bundle (vite build --watch) triggers a normal in-place reload
  • per-kernel bookkeeping dropped on kernel close; _modules mutation locked

Everything is hasattr(ipyvue, "define_module")-gated, so it is a no-op on current ipyvue releases; the unit tests skip likewise until an ipyvue release ships the feature (verified 5/5 passing locally against the ipyvue#106 branch).

Url modules: preload + content-hash caching

define_module takes exactly one of module (a Path), code= or url= (nothing is guessed from a plain str); url= serves the bundle from the app's static dir. For urls solara serves itself (/static/public/...):

  • the page emits <link rel="modulepreload" href=".../bundle.mjs?v=<content-hash>">, so the browser fetches the bundle in parallel with kernel startup instead of waiting for the Module widget over the websocket (es-module-shims honors modulepreload in shim mode);
  • the same versioned url is used for the hint and the widget (one helper, esm_vue.versioned_url), so the preload is always a cache hit;
  • StaticPublic serves the file with Cache-Control: max-age=1y, immutable only when the requested ?v matches the current content hash — rebuilds change the hash and therefore the url (automatic cache busting), stale hashes get normal headers (no poisoning). External urls and urls with their own query string pass through untouched.

This generalizes the existing nbextension-hash mechanism (nbextensions_hashes + requirejs urlArgs) to app-served ES modules.

Documentation

Includes a website howto page (documentation/advanced/howto/vue-esm-components) with a complete runnable example (verified end to end locally against the ipyvue#106 branch: renders, click events reach Python, trait updates re-render) plus the vite workflow and the tag form.

Usage (once ipyvue#106 is released)

solara.component_vue gains esm_module/esm_export as an alternative to vue_path:

import ipyvue
import solara

# build .vue SFCs ahead of time with vite (vue external, npm deps bundled);
# solara watches the file, so `vite build --watch` gives in-place hot reload
ipyvue.define_module("my-components", Path(__file__).parent / "dist/my-components.mjs")


@solara.component_vue(esm_module="my-components", esm_export="Counter")
def Counter(count: int = 0, event_bump: Callable[[int], None] = None):
    pass

Same contract as a .vue-file component: arguments become Vue data (overriding the component's own data() placeholders), event_* arguments become callable methods. The howto page in this PR contains a complete no-build-step runnable example (verified end to end against the ipyvue#106 branch: renders, clicks reach Python, state re-renders) plus the vite workflow and the tag form.

No template source goes over the wire and vue/compiler-sfc is not needed at runtime for these components. Coexists with ipyreact modules on the same page (verified; registries are separate, the import-map namespace is shared so module names must be unique across libraries).

@maartenbreddels
maartenbreddels force-pushed the feat/ipyvue-esm-modules branch from aaba0b0 to 467fb56 Compare July 4, 2026 14:04
@maartenbreddels
maartenbreddels force-pushed the feat/ipyvue-esm-modules branch from 467fb56 to 48474bf Compare July 4, 2026 14:18
@maartenbreddels
maartenbreddels temporarily deployed to feat/ipyvue-esm-modules - solara-stable PR #1169 July 4, 2026 19:54 — with Render Destroyed
maartenbreddels added a commit to widgetti/ipyreact that referenced this pull request Jul 4, 2026
define_module(name, "/static/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, cacheable and preloadable). A plain
str now always means a url; inline source moves to the explicit code
keyword.

Note: under solara server define_module is patched by solara.server.esm,
which needs the matching update (widgetti/solara#1169) for the new
signature to apply there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maartenbreddels and others added 6 commits July 5, 2026 10:15
ipyvue's vue3 branch gains define_module for precompiled ESM bundles
(vite-built .vue components); this provides the same server-side
handling ipyreact modules get in esm.py: definitions recorded once,
Module widgets materialized per kernel (recreated when context.restart
closed them, so in-place reload works), bundle Paths watched by the
reloader, per-kernel bookkeeping dropped on kernel close.

No-op for ipyvue without ES module support (hasattr-gated); the unit
tests skip likewise until an ipyvue release ships it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
define_module accepts a url (e.g. a bundle served from the app's static
dir); the page emits <link rel="modulepreload"> for url-backed modules,
so the browser fetches them in parallel with kernel startup instead of
waiting for the Module widget to arrive over the websocket
(es-module-shims honors modulepreload hints in shim mode).

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>
versioned_url appends ?v=<content-hash> to module urls that solara
serves itself (/static/public/...); the SAME url is used for the
modulepreload hint in the page and the Module widget, so the preload is
always a cache hit. StaticPublic sends max-age=1y, immutable when the
requested hash matches the current file content - any rebuild changes
the url, so caching can be aggressive without staleness. Urls with an
existing query string and external urls pass through untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…serving

ipyreact modules get the same treatment as ipyvue ones: define_module
accepts a url (str = url, code= for inline source), the widget gets the
content-hash-versioned url, and the page preloads it. versioned_url
moves to server.py so esm.py does not pull in ipyvue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exactly one of module (a Path), code= or url= must be passed, matching
the ipyvue/ipyreact signatures; nothing is guessed from a plain str.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@maartenbreddels
maartenbreddels force-pushed the feat/ipyvue-esm-modules branch from 576e18a to a684fd7 Compare July 5, 2026 08:15
maartenbreddels added a commit to widgetti/ipyreact that referenced this pull request Jul 5, 2026
define_module(name, url="/static/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, cacheable and preloadable).
Exactly one of module (a Path), code= or url= must be passed; a plain
str still works as module code for backwards compatibility, with a
DeprecationWarning.

Note: under solara server define_module is patched by solara.server.esm,
which needs the matching update (widgetti/solara#1169) for the new
signature to apply there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches ipyreact's own deprecation path: a plain str was module code in
the released api, so it keeps working with a DeprecationWarning instead
of a hard error. The ipyvue variant stays strict (never released).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@maartenbreddels
maartenbreddels temporarily deployed to feat/ipyvue-esm-modules - solara-stable PR #1169 July 5, 2026 10:33 — with Render Destroyed
maartenbreddels added a commit to widgetti/ipyreact that referenced this pull request Jul 5, 2026
define_module(name, url="/static/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, cacheable and preloadable).
Exactly one of module (a Path), code= or url= must be passed; a plain
str still works as module code for backwards compatibility, with a
DeprecationWarning.

Note: under solara server define_module is patched by solara.server.esm,
which needs the matching update (widgetti/solara#1169) for the new
signature to apply there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maartenbreddels added a commit to widgetti/ipyreact that referenced this pull request Jul 5, 2026
define_module(name, url="/static/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, cacheable and preloadable).
Exactly one of module (a Path), code= or url= must be passed; a plain
str still works as module code for backwards compatibility, with a
DeprecationWarning.

Note: under solara server define_module is patched by solara.server.esm,
which needs the matching update (widgetti/solara#1169) for the new
signature to apply there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@maartenbreddels
maartenbreddels merged commit ee24330 into master Jul 5, 2026
65 of 66 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