From e55ad2a1b9ef720067ff37ec25e460e1e01d17eb Mon Sep 17 00:00:00 2001 From: darshan Date: Sat, 27 Jun 2026 17:47:43 +0545 Subject: [PATCH 1/5] clear-err-on-older-nodejs --- jupyter_builder/federated_extensions.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/jupyter_builder/federated_extensions.py b/jupyter_builder/federated_extensions.py index eff1f95..2f820f2 100644 --- a/jupyter_builder/federated_extensions.py +++ b/jupyter_builder/federated_extensions.py @@ -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() @@ -238,6 +240,7 @@ 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) if marker_pkg == "@jupyterlab/builder": core_flag = ["--core-path", _resolve_core_path_for_jupyterlab_builder(core_package_file)] @@ -296,6 +299,7 @@ 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) if marker_pkg == "@jupyterlab/builder": core_flag = ["--core-path", _resolve_core_path_for_jupyterlab_builder(core_package_file)] @@ -320,6 +324,57 @@ 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" + + +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}). " + "'@rspack/core' is an ES module that older Node.js versions cannot load. " + "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. From 6e20708fa3267ed8ceef362a9ef92e34b9454b06 Mon Sep 17 00:00:00 2001 From: darshan Date: Sat, 27 Jun 2026 17:47:56 +0545 Subject: [PATCH 2/5] add-tests --- tests/test_core_path.py | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/tests/test_core_path.py b/tests/test_core_path.py index ba6a5b4..bfb4a7b 100644 --- a/tests/test_core_path.py +++ b/tests/test_core_path.py @@ -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: @@ -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" + + +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)) From f5deb8041dbac4ad4365d558c2498a9c29c18b3f Mon Sep 17 00:00:00 2001 From: Darshan Poudel Date: Mon, 29 Jun 2026 07:31:39 +0545 Subject: [PATCH 3/5] Update jupyter_builder/federated_extensions.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: MichaƂ Krassowski <5832902+krassowski@users.noreply.github.com> --- jupyter_builder/federated_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyter_builder/federated_extensions.py b/jupyter_builder/federated_extensions.py index 2f820f2..cc2246d 100644 --- a/jupyter_builder/federated_extensions.py +++ b/jupyter_builder/federated_extensions.py @@ -368,7 +368,6 @@ def _check_node_version( return msg = ( f"Building this extension requires Node.js {node_range} (found {raw}). " - "'@rspack/core' is an ES module that older Node.js versions cannot load. " "Please upgrade Node.js." ) if logger: From b2bbb7b47d361ad424dc5995bd0eb5d6776b399a Mon Sep 17 00:00:00 2001 From: Darshan Poudel Date: Wed, 1 Jul 2026 14:18:40 +0545 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jupyter_builder/federated_extensions.py | 2 +- tests/test_core_path.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_builder/federated_extensions.py b/jupyter_builder/federated_extensions.py index cc2246d..e6845b9 100644 --- a/jupyter_builder/federated_extensions.py +++ b/jupyter_builder/federated_extensions.py @@ -327,7 +327,7 @@ def watch_labextension( # noqa: PLR0913 # 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" +_FALLBACK_NODE_RANGE = "^20.19.0 || >=22.12.0" def _read_rspack_node_range(builder: str, ext_path: str) -> str: diff --git a/tests/test_core_path.py b/tests/test_core_path.py index bfb4a7b..927744a 100644 --- a/tests/test_core_path.py +++ b/tests/test_core_path.py @@ -363,7 +363,7 @@ 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" + 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): From 22f36b7983c6d0cc49dd951707dadf5332bb8251 Mon Sep 17 00:00:00 2001 From: darshan Date: Wed, 1 Jul 2026 14:17:53 +0545 Subject: [PATCH 5/5] remove-hard-coded-node --- jupyter_builder/federated_extensions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jupyter_builder/federated_extensions.py b/jupyter_builder/federated_extensions.py index e6845b9..5e100ab 100644 --- a/jupyter_builder/federated_extensions.py +++ b/jupyter_builder/federated_extensions.py @@ -247,7 +247,8 @@ def build_labextension( # noqa: PLR0913 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: @@ -306,7 +307,8 @@ def watch_labextension( # noqa: PLR0913 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: