Skip to content

Instantly share code, notes, and snippets.

@jlevy
Last active January 8, 2025 09:49
Show Gist options
  • Save jlevy/450f65ef9d83f8af399dca23c4619468 to your computer and use it in GitHub Desktop.
Save jlevy/450f65ef9d83f8af399dca23c4619468 to your computer and use it in GitHub Desktop.
xonsh xontrib for fnm (for node version management)
"""
xontrib for fnm (Fast Node Manager) integration with xonsh shell.
Fnm is a good replacement for nvm and is compatible with .nvmrc.
Currently fnm doesn't support xonsh directly. This parses the bash output
of fnm and uses it within xonsh.
"""
import subprocess
from pathlib import Path
from typing import Optional, TypedDict
from xonsh.built_ins import XonshSession
from xonsh.events import events
class FnmEnv(TypedDict):
PATH: str
FNM_MULTISHELL_PATH: str
FNM_VERSION_FILE_STRATEGY: str
FNM_DIR: str
FNM_LOGLEVEL: str
FNM_NODE_DIST_MIRROR: str
FNM_COREPACK_ENABLED: str
FNM_RESOLVE_ENGINES: str
FNM_ARCH: str
def parse_fnm_env() -> FnmEnv:
"""
Parse fnm bash env output into a typed dictionary.
"""
result = subprocess.run(
["fnm", "env", "--use-on-cd", "--shell", "bash"],
capture_output=True,
text=True,
check=True,
)
env_vars: FnmEnv = {} # type: ignore
for line in result.stdout.strip().split("\n"):
if line.startswith("export "):
key, value = line.replace("export ", "").split("=", 1)
env_vars[key] = value.strip('"')
return env_vars
def apply_fnm_env(xsh: XonshSession, env_vars: FnmEnv) -> None:
"""
Apply fnm environment variables to xonsh session.
"""
# Update PATH with multishell bin
multishell_bin = Path(env_vars["FNM_MULTISHELL_PATH"]) / "bin"
assert xsh.env is not None
path = xsh.env["PATH"]
assert path is not None
current_paths = [Path(p) for p in path]
new_paths = [p for p in current_paths if "fnm_multishells" not in str(p)]
new_paths.insert(0, multishell_bin)
xsh.env["PATH"] = new_paths
# Set all other fnm variables
for key, value in env_vars.items():
if key != "PATH":
xsh.env[key] = value
def _nvm_stub(args: str, stdin: str, stdout: str, stderr: str) -> None:
print("error: Oops, `nvm` doesn't work in xonsh; try `fnm` instead!")
def _load_xontrib_(xsh: XonshSession, **_) -> dict:
"""
Initialize the fnm xontrib.
"""
try:
subprocess.run(["which", "fnm"], capture_output=True, check=True)
except subprocess.CalledProcessError:
# Quietly return if fnm is not installed.
return {}
try:
# Set fnm per-session environment variables once at initialization.
env_vars = parse_fnm_env()
apply_fnm_env(xsh, env_vars)
def fnm_use_if_file_found(newdir: str, olddir: Optional[str] = None) -> None:
"""
Check for Node version files and switch if necessary using xsh session.
"""
version_files = [".node-version", ".nvmrc", "package.json"]
if any(Path(file).exists() for file in version_files):
xsh.subproc_uncaptured(["fnm", "use", "--silent-if-unchanged"])
events.on_chdir(fnm_use_if_file_found)
fnm_use_if_file_found(".")
aliases["nvm"] = _nvm_stub # type: ignore # noqa: F821
return {}
except Exception as e:
print(f"Error initializing fnm xontrib: {e}")
return {}
def _unload_xontrib_(xsh: XonshSession, **_) -> dict:
"""
Clean up the fnm xontrib.
"""
handlers_to_remove = [
handler
for handler in events.on_chdir.handlers
if handler.__name__ == "fnm_use_if_file_found"
]
for handler in handlers_to_remove:
events.on_chdir.remove(handler)
return {}
@anki-code
Copy link

Hey! Good work!
It will be cool to create xontrib-fnm repository for this instead of gist to have an ability to contribute to the project.
We have a template to create full featured xontrib in 10 minutes - https://github.com/xonsh/xontrib-template - please take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment