Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion marimo/_messaging/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

from __future__ import annotations

import re
import time
from typing import (
Any,
ClassVar,
Literal,
)
from urllib.parse import urlsplit

import msgspec

Expand Down Expand Up @@ -48,6 +50,28 @@

LOGGER = loggers.marimo_logger()

_VIRTUAL_FILE_URL_RE = re.compile(r"^(?:\.?/)?@file/[^?#]+$")
_JAVASCRIPT_DATA_URL_RE = re.compile(
r"^data:(?:text|application)/javascript(?:;[^,]*)?,[^\r\n]*$"
)


def _normalize_esm_url(value: str) -> str | None:
if _VIRTUAL_FILE_URL_RE.fullmatch(value) is not None:
return value
if _JAVASCRIPT_DATA_URL_RE.fullmatch(value) is not None:
return value
if any(character.isspace() for character in value):
return None

try:
parsed = urlsplit(value)
except ValueError:
return None
if parsed.scheme in ("http", "https") and parsed.netloc:
return parsed.geturl()
return None


class Notification(msgspec.Struct, tag_field="op"):
"""Base class for all kernel-to-frontend notifications.
Expand Down Expand Up @@ -202,7 +226,10 @@ def from_esm(esm: str) -> EsmSpec:
import marimo._output.data.data as mo_data
from marimo._utils.code import hash_code

return EsmSpec(url=mo_data.js(esm).url, hash=hash_code(esm))
url = _normalize_esm_url(esm)
if url is None:
url = mo_data.any_data(esm.encode("utf-8"), ext="js").url
return EsmSpec(url=url, hash=hash_code(esm))


class ModelOpen(msgspec.Struct, tag="open", tag_field="method"):
Expand Down
27 changes: 27 additions & 0 deletions tests/_plugins/ui/_impl/test_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,33 @@ def test_url_form_esm_passes_through(self, comm_manager):
assert message.esm_spec.url == url
assert message.esm_spec.hash == hash_code(url)

def test_url_form_esm_normalizes_scheme(self, comm_manager):
url = "HTTPS://esm.sh/some-widget@1.0.0"
_, message = self._open_comm(comm_manager, {"_esm": url})

assert message.esm_spec is not None
assert message.esm_spec.url == "https://esm.sh/some-widget@1.0.0"

def test_virtual_file_url_esm_passes_through(self, comm_manager):
from marimo._utils.code import hash_code

url = "./@file/64-bundle.js"
_, message = self._open_comm(comm_manager, {"_esm": url})

assert message.esm_spec is not None
assert message.esm_spec.url == url
assert message.esm_spec.hash == hash_code(url)

def test_url_like_inline_esm_is_minted(self, comm_manager):
esm = "data: {\n const value = 1;\n}\nexport default {};"
_, message = self._open_comm(comm_manager, {"_esm": esm})

assert message.esm_spec is not None
assert message.esm_spec.url.startswith(
"data:application/javascript;base64,"
)
assert message.esm_spec.url != esm

def test_no_esm_means_no_spec(self, comm_manager):
comm, message = self._open_comm(comm_manager, {"value": 5})

Expand Down
Loading