Skip to content
Merged
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
60 changes: 58 additions & 2 deletions jupyter_builder/federated_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

from .commands import _test_overlap
from .core_path import get_core_meta
from .jlpm import _which_node_js
from .jupyterlab_semver import clean, satisfies

DEPRECATED_ARGUMENT = object()

Expand Down Expand Up @@ -238,13 +240,15 @@ def build_labextension( # noqa: PLR0913
logger.info("Building extension in %s", path)

builder, marker_pkg = _ensure_builder(ext_path, core_package_file)
_check_node_version(builder, ext_path, logger=logger)
Comment thread
Darshan808 marked this conversation as resolved.

if marker_pkg == "@jupyterlab/builder":
core_flag = ["--core-path", _resolve_core_path_for_jupyterlab_builder(core_package_file)]
else:
core_flag = ["--core-package-file", core_package_file]

arguments = ["node", builder, *core_flag, ext_path]
node = _which_node_js()
arguments = [node, builder, *core_flag, ext_path]
if static_url is not None:
arguments.extend(["--static-url", static_url])
if development:
Expand Down Expand Up @@ -296,13 +300,15 @@ def watch_labextension( # noqa: PLR0913
Path(full_dest).symlink_to(output_dir)

builder, marker_pkg = _ensure_builder(ext_path, core_package_file)
_check_node_version(builder, ext_path, logger=logger)
Comment thread
Darshan808 marked this conversation as resolved.

if marker_pkg == "@jupyterlab/builder":
core_flag = ["--core-path", _resolve_core_path_for_jupyterlab_builder(core_package_file)]
else:
core_flag = ["--core-package-file", core_package_file]

arguments = ["node", builder, *core_flag, "--watch", ext_path]
node = _which_node_js()
arguments = [node, builder, *core_flag, "--watch", ext_path]
if development:
arguments.append("--development")
if source_map:
Expand All @@ -320,6 +326,56 @@ def watch_labextension( # noqa: PLR0913
# of preference.
_BUILDER_MARKER_CANDIDATES = ("@jupyter/builder", "@jupyterlab/builder")

# Minimum Node.js range required by `@rspack/core` when its own `engines.node`
# field cannot be read. `@rspack/core` is a pure ES module that older Node.js
# versions cannot `require()`.
_FALLBACK_NODE_RANGE = "^20.19.0 || >=22.12.0"


def _read_rspack_node_range(builder: str, ext_path: str) -> str:
"""Return the ``engines.node`` range declared by ``@rspack/core``."""
for root in (Path(builder).parent, Path(ext_path)):
target = root
while True:
pkg = target / "node_modules" / "@rspack" / "core" / "package.json"
if pkg.exists():
try:
with pkg.open() as fid:
node_range = json.load(fid).get("engines", {}).get("node")
except (OSError, ValueError):
node_range = None
if node_range:
return str(node_range)
break
if target.parent == target:
break
target = target.parent
return _FALLBACK_NODE_RANGE


def _check_node_version(
builder: str,
ext_path: str,
logger: logging.Logger | None = None,
) -> None:
"""Fail early with a clear message when Node.js is too old to load the builder."""
node = _which_node_js()
node_range = _read_rspack_node_range(builder, ext_path)
try:
raw = subprocess.check_output([node, "--version"]).decode("utf8").strip() # noqa: S603
except (OSError, subprocess.CalledProcessError):
return
current = clean(raw, loose=True) # type: ignore[no-untyped-call]
if current is None or satisfies(current, node_range, loose=True): # type: ignore[no-untyped-call]
return
msg = (
f"Building this extension requires Node.js {node_range} (found {raw}). "
"Please upgrade Node.js."
)
if logger:
logger.error(msg)
raise RuntimeError(msg)


def _resolve_core_path_for_jupyterlab_builder(core_package_file: str) -> str:
"""Return the core path directory for @jupyterlab/builder.
Expand Down
61 changes: 59 additions & 2 deletions tests/test_core_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@

import pytest

from jupyter_builder import core_path
from jupyter_builder.federated_extensions import _ensure_builder
from jupyter_builder import core_path, federated_extensions
from jupyter_builder.federated_extensions import (
_check_node_version,
_ensure_builder,
_read_rspack_node_range,
)


def _make_core_package_tarball(content: bytes) -> bytes:
Expand Down Expand Up @@ -338,3 +342,56 @@ def test_ensure_builder_with_jupyterlab_builder(tmp_path):

assert builder_path == str(builder_dir / "lib" / "build-labextension.js")
assert marker_pkg == "@jupyterlab/builder"


def _write_rspack(ext_path: Path, node_range: str) -> None:
rspack_dir = ext_path / "node_modules" / "@rspack" / "core"
rspack_dir.mkdir(parents=True)
(rspack_dir / "package.json").write_text(json.dumps({"engines": {"node": node_range}}))


def test_read_rspack_node_range_reads_engines(tmp_path):
ext_path = tmp_path / "ext"
ext_path.mkdir()
_write_rspack(ext_path, "^20.19.0 || >=22.12.0")

builder = str(ext_path / "node_modules" / "@jupyter" / "builder" / "lib" / "x.js")
assert _read_rspack_node_range(builder, str(ext_path)) == "^20.19.0 || >=22.12.0"


def test_read_rspack_node_range_falls_back_when_missing(tmp_path):
ext_path = tmp_path / "ext"
ext_path.mkdir()

assert _read_rspack_node_range(str(ext_path), str(ext_path)) == "^20.19.0 || >=22.12.0"


def test_check_node_version_raises_on_old_node(tmp_path, monkeypatch):
ext_path = tmp_path / "ext"
ext_path.mkdir()
_write_rspack(ext_path, "^20.19.0 || >=22.12.0")

monkeypatch.setattr(federated_extensions, "_which_node_js", lambda: "node")
monkeypatch.setattr(
federated_extensions.subprocess,
"check_output",
lambda *_args, **_kwargs: b"v18.20.8\n",
)

with pytest.raises(RuntimeError, match=r"requires Node\.js .* \(found v18\.20\.8\)"):
_check_node_version(str(ext_path), str(ext_path))


def test_check_node_version_passes_on_supported_node(tmp_path, monkeypatch):
ext_path = tmp_path / "ext"
ext_path.mkdir()
_write_rspack(ext_path, "^20.19.0 || >=22.12.0")

monkeypatch.setattr(federated_extensions, "_which_node_js", lambda: "node")
monkeypatch.setattr(
federated_extensions.subprocess,
"check_output",
lambda *_args, **_kwargs: b"v22.12.0\n",
)

_check_node_version(str(ext_path), str(ext_path))
Loading