From 78db417a683d616fc66e7fc08fb4f9c39006a190 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:49:39 -0400 Subject: [PATCH 1/3] WIP: cdn fallbacks --- src/vitessce/widget.py | 89 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/src/vitessce/widget.py b/src/vitessce/widget.py index b1b3e066..c86df353 100644 --- a/src/vitessce/widget.py +++ b/src/vitessce/widget.py @@ -156,6 +156,11 @@ def get_uid_str(uid): # lang: js ESM = """ import { importWithMap } from 'https://unpkg.com/dynamic-importmap@0.1.0'; +const successfulImportMap = { + imports: { + + }, +}; const importMap = { imports: { "react": "https://esm.sh/react@18.2.0?dev", @@ -163,9 +168,67 @@ def get_uid_str(uid): "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev", }, }; +const fallbackImportMap = { + imports: { + //"react": "https://cdn.vitessce.io/react@18.2.0/index.js", + //"react-dom": "https://cdn.vitessce.io/react-dom@18.2.0/index.js", + //"react-dom/client": "https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.mjs", + + // Replace with version-specific URL below. + "vitessce": "https://cdn.vitessce.io/vitessce@3.8.5/dist/index.min.js", + }, +}; +/* +const fallbackDevImportMap = { + imports: { + "react": "https://cdn.vitessce.io/react@18.2.0/index_dev.js", + "react-dom": "https://cdn.vitessce.io/react-dom@18.2.0/index_dev.js", + "react-dom/client": "https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.development.mjs", + // Replace with version-specific URL below. + "vitessce": "https://cdn.vitessce.io/@vitessce/dev@3.8.5/dist/index.js", + }, +}; +*/ + +async function importWithMapAndFallback(moduleName, importMap, fallbackMap) { + console.log(`Importing ${moduleName} with import map`, importMap, fallbackMap); + + let result = null; + if (!fallbackMap) { + // fallbackMap is null, user may have provided custom JS URL. + result = await importWithMap(moduleName, { + imports: { + ...importMap.imports, + ...successfulImportMap.imports, + }, + }); + successfulImportMap.imports[moduleName] = importMap.imports[moduleName]; + } else { + try { + result = await importWithMap(moduleName, { + imports: { + ...importMap.imports, + ...successfulImportMap.imports, + }, + }); + successfulImportMap.imports[moduleName] = importMap.imports[moduleName]; + } catch (e) { + console.warn(`Importing ${moduleName} failed with import map`, importMap, e); + result = await importWithMap(moduleName, { + imports: { + ...fallbackMap.imports, + ...successfulImportMap.imports, + }, + }); + successfulImportMap.imports[moduleName] = fallbackMap.imports[moduleName]; + } + } + return result; +} -const React = await importWithMap("react", importMap); -const { createRoot } = await importWithMap("react-dom/client", importMap); + +const React = await importWithMapAndFallback("react", importMap, fallbackImportMap); +const { createRoot } = await importWithMapAndFallback("react-dom/client", importMap, fallbackImportMap); const e = React.createElement; @@ -252,6 +315,7 @@ def get_uid_str(uid): } async function render(view) { + console.log("Rendering Vitessce widget..."); const cssUid = view.model.get('uid'); const jsDevMode = view.model.get('js_dev_mode'); const jsPackageVersion = view.model.get('js_package_version'); @@ -268,10 +332,21 @@ def get_uid_str(uid): const pkgName = (jsDevMode ? "@vitessce/dev" : "vitessce"); - importMap.imports["vitessce"] = (customJsUrl.length > 0 + const hasCustomJsUrl = customJsUrl.length > 0; + + importMap.imports["vitessce"] = (hasCustomJsUrl ? customJsUrl : `https://unpkg.com/${pkgName}@${jsPackageVersion}` ); + let fallbackImportMapToUse = null; + if (!hasCustomJsUrl) { + fallbackImportMapToUse = fallbackImportMap; + if (jsDevMode) { + fallbackImportMapToUse.imports["vitessce"] = `https://cdn.vitessce.io/vitessce@${jsPackageVersion}/dist/index.min.js`; + } else { + fallbackImportMapToUse.imports["vitessce"] = `https://cdn.vitessce.io/@vitessce/dev@${jsPackageVersion}/dist/index.js`; + } + } const { Vitessce, @@ -292,7 +367,7 @@ def get_uid_str(uid): useComplexCoordinationSecondary, useCoordinationScopes, useCoordinationScopesBy, - } = await importWithMap("vitessce", importMap); + } = await importWithMapAndFallback("vitessce", importMap, fallbackImportMapToUse); let pluginViewTypes = []; let pluginCoordinationTypes = []; @@ -651,7 +726,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('3.8.3').tag(sync=True) + js_package_version = Unicode('3.8.5').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -664,7 +739,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.8.3', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, prefer_local=True, invoke_timeout=300000, invoke_batched=True, page_mode=False, page_esm=None, prevent_scroll=True, server_host=None): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.8.5', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, prefer_local=True, invoke_timeout=300000, invoke_batched=True, page_mode=False, page_esm=None, prevent_scroll=True, server_host=None): """ Construct a new Vitessce widget. Not intended to be instantiated directly; instead, use ``VitessceConfig.widget``. @@ -798,7 +873,7 @@ def _plugin_command(self, params, buffers): # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.8.3', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None, server_host=None): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.8.5', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None, server_host=None): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) From 68a5d8455489645bc7734b0093dbc5e7328a20db Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:03:36 -0400 Subject: [PATCH 2/3] Fallback for dynamic-importmap --- src/vitessce/widget.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/vitessce/widget.py b/src/vitessce/widget.py index c86df353..c26d4b32 100644 --- a/src/vitessce/widget.py +++ b/src/vitessce/widget.py @@ -155,7 +155,14 @@ def get_uid_str(uid): # lang: js ESM = """ -import { importWithMap } from 'https://unpkg.com/dynamic-importmap@0.1.0'; +let importWithMap; +try { + importWithMap = (await import('https://unpkg.com/dynamic-importmap@0.1.0')).importWithMap; +} catch(e) { + console.warn("Import of dynamic-importmap failed, trying fallback."); + importWithMap = (await import('https://cdn.vitessce.io/dynamic-importmap@0.1.0/dist/index.js')).importWithMap; +} + const successfulImportMap = { imports: { @@ -170,11 +177,10 @@ def get_uid_str(uid): }; const fallbackImportMap = { imports: { - //"react": "https://cdn.vitessce.io/react@18.2.0/index.js", - //"react-dom": "https://cdn.vitessce.io/react-dom@18.2.0/index.js", - //"react-dom/client": "https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.mjs", - - // Replace with version-specific URL below. + "react": "https://cdn.vitessce.io/react@18.2.0/index.js", + "react-dom": "https://cdn.vitessce.io/react-dom@18.2.0/index.js", + "react-dom/client": "https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.mjs", + // Replaced with version-specific URL below. "vitessce": "https://cdn.vitessce.io/vitessce@3.8.5/dist/index.min.js", }, }; @@ -184,15 +190,13 @@ def get_uid_str(uid): "react": "https://cdn.vitessce.io/react@18.2.0/index_dev.js", "react-dom": "https://cdn.vitessce.io/react-dom@18.2.0/index_dev.js", "react-dom/client": "https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.development.mjs", - // Replace with version-specific URL below. + // Replaced with version-specific URL below. "vitessce": "https://cdn.vitessce.io/@vitessce/dev@3.8.5/dist/index.js", }, }; */ async function importWithMapAndFallback(moduleName, importMap, fallbackMap) { - console.log(`Importing ${moduleName} with import map`, importMap, fallbackMap); - let result = null; if (!fallbackMap) { // fallbackMap is null, user may have provided custom JS URL. @@ -213,7 +217,7 @@ def get_uid_str(uid): }); successfulImportMap.imports[moduleName] = importMap.imports[moduleName]; } catch (e) { - console.warn(`Importing ${moduleName} failed with import map`, importMap, e); + console.warn(`Importing ${moduleName} failed with importMap`, importMap, "trying fallback", fallbackMap, successfulImportMap); result = await importWithMap(moduleName, { imports: { ...fallbackMap.imports, @@ -315,7 +319,6 @@ def get_uid_str(uid): } async function render(view) { - console.log("Rendering Vitessce widget..."); const cssUid = view.model.get('uid'); const jsDevMode = view.model.get('js_dev_mode'); const jsPackageVersion = view.model.get('js_package_version'); From ec4f722abbffa9d7eb0931723f5584515de5739c Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:05:56 -0400 Subject: [PATCH 3/3] Version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3051e3a5..54b34d5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "vitessce" -version = "3.7.6" +version = "3.7.7" authors = [ { name="Mark Keller", email="mark_keller@hms.harvard.edu" }, ]