diff --git a/backend/app/app_compile_contract.py b/backend/app/app_compile_contract.py index bac1408ca..f42a38bd3 100644 --- a/backend/app/app_compile_contract.py +++ b/backend/app/app_compile_contract.py @@ -101,10 +101,17 @@ def runtime_library_aliases() -> tuple[tuple[str, Path], ...]: ``app_runtime_inject.js``. React then sees two dispatchers and every hook fails at first render. Package-root aliases apply to the root and its subpaths (for example ``react/jsx-runtime``), keeping each supported library singular. + + Three's documented ``three/addons/*`` export is backed by the physical + ``examples/jsm`` directory rather than an ``addons`` directory. Once the + package root is replaced with an absolute alias, esbuild no longer consults + Three's package exports for that subpath. Pin the public addons spelling to + its runtime-owned physical directory before adding the package roots. """ node_path = runtime_node_path() roots = sorted({_package_root(specifier) for specifier in BUNDLED_RUNTIME_LIBS}) - return tuple((root, node_path / root) for root in roots) + subpaths = (("three/addons", node_path / "three" / "examples" / "jsm"),) + return subpaths + tuple((root, node_path / root) for root in roots) NO_DEFAULT_EXPORT_ERROR = ( diff --git a/backend/tests/test_runtime_libs.py b/backend/tests/test_runtime_libs.py index 17690a054..cbda06c26 100644 --- a/backend/tests/test_runtime_libs.py +++ b/backend/tests/test_runtime_libs.py @@ -21,6 +21,7 @@ mobius_runtime_path, runtime_library_aliases, runtime_inject_path, + runtime_node_path, ) @@ -121,6 +122,50 @@ def test_app_local_or_transitive_react_cannot_shadow_platform_runtime(tmp_path): assert "shadow-react-copy" not in output.read_text() +def test_three_addons_resolve_from_the_pinned_runtime(tmp_path): + """Documented addons imports must survive package-root runtime pinning.""" + aliases = dict(runtime_library_aliases()) + assert aliases["three"] == runtime_node_path() / "three" + assert ( + aliases["three/addons"] + == runtime_node_path() / "three" / "examples" / "jsm" + ) + + entry = tmp_path / "three-addons.jsx" + output = tmp_path / "three-addons.js" + metafile = tmp_path / "three-addons-meta.json" + entry.write_text( + """import { OrbitControls } from 'three/addons/controls/OrbitControls.js' +import { STLLoader } from 'three/addons/loaders/STLLoader.js' + +export default function ThreeAddonsFixture() { + return [OrbitControls.name, STLLoader.name] +} +""" + ) + + completed = subprocess.run( + esbuild_command(entry, output, metafile=metafile), + capture_output=True, + check=False, + env=esbuild_environment(), + text=True, + timeout=ESBUILD_TIMEOUT_SECS, + ) + assert completed.returncode == 0, completed.stderr + + metadata = json.loads(metafile.read_text()) + entry_outputs = [ + details for details in metadata["outputs"].values() + if details.get("entryPoint") + ] + assert len(entry_outputs) == 1 + assert entry_outputs[0].get("imports") == [], ( + "Three addons escaped the pinned self-contained app bundle" + ) + assert output.is_file() and output.stat().st_size > 0 + + def test_app_hosts_have_no_runtime_import_map_or_static_module_imports(): frame = FRAME.read_text() standalone = STANDALONE.read_text()