-
Notifications
You must be signed in to change notification settings - Fork 639
feat: add default-packages file support for fnm install #1503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
c756518
6079dc8
56ecb63
0bb3019
225db07
cac77b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "fnm": minor | ||
| --- | ||
|
|
||
| Added support for `default-packages` file. When present at `$FNM_DIR/default-packages`, packages listed in this file are automatically installed globally after every `fnm install`. Compatible with nvm's default-packages format. | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -270,6 +270,9 @@ Options: | |||||||||||||
|
|
||||||||||||||
| [env: FNM_ARCH] | ||||||||||||||
|
|
||||||||||||||
| --reinstall-packages-from <version> | ||||||||||||||
| Reinstall global packages from a specified Node version after installing. Analogous to nvm's --reinstall-packages-from flag | ||||||||||||||
|
|
||||||||||||||
| --version-file-strategy <VERSION_FILE_STRATEGY> | ||||||||||||||
| A strategy for how to resolve the Node version. Used whenever `fnm use` or `fnm install` is called without a version, or when `--use-on-cd` is configured on evaluation | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -414,6 +417,10 @@ Options: | |||||||||||||
| --use-on-cd | ||||||||||||||
| Print the script to change Node versions every directory change | ||||||||||||||
|
|
||||||||||||||
| When entering a directory with a version file, fnm switches to that version. When entering a directory without a version file, fnm switches to the default version. | ||||||||||||||
|
|
||||||||||||||
| This applies to both `local` and `recursive` version file strategies. | ||||||||||||||
|
||||||||||||||
| When entering a directory with a version file, fnm switches to that version. When entering a directory without a version file, fnm switches to the default version. | |
| This applies to both `local` and `recursive` version file strategies. | |
| When entering a directory with a version file, fnm switches to that version. Behavior when entering a directory without a version file depends on the configured version file strategy. | |
| With the `recursive` strategy, fnm walks up parent directories to find a version file and switches to the default version when none is found. With the `local` strategy, fnm only switches versions when a version file is present in the current directory. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -52,6 +52,14 @@ error: Can't find version in dotfiles. Please provide a version manually to the | |||||
|
|
||||||
| Runs [`corepack enable`](https://nodejs.org/api/corepack.html#enabling-the-feature) when a new version of Node.js is installed. Experimental due to the fact Corepack itself is experimental. | ||||||
|
|
||||||
| ### `default-packages` file | ||||||
|
|
||||||
| When present at `$FNM_DIR/default-packages` (or `~/.local/share/fnm/default-packages`), fnm will automatically install packages listed in this file globally after every `fnm install` by running `npm install -g`. | ||||||
|
||||||
| When present at `$FNM_DIR/default-packages` (or `~/.local/share/fnm/default-packages`), fnm will automatically install packages listed in this file globally after every `fnm install` by running `npm install -g`. | |
| When present at `$FNM_DIR/default-packages`, fnm will automatically install packages listed in this file globally after every `fnm install` by running `npm install -g`. You can run `fnm env` to see the value of `$FNM_DIR` on your system. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
|
||
| exports[`Bash installs default packages: Bash 1`] = ` | ||
| "set -e | ||
| eval "$(fnm env)" | ||
| fnm install 18 | ||
| (fnm exec --using=18 npm list -g --depth=0) | grep 'is-odd' || (echo "Expected output to contain 'is-odd'" && exit 1)" | ||
| `; | ||
|
|
||
| exports[`Bash missing default-packages file does not error: Bash 1`] = ` | ||
| "set -e | ||
| eval "$(fnm env)" | ||
| fnm install 18" | ||
| `; | ||
|
|
||
| exports[`PowerShell installs default packages: PowerShell 1`] = ` | ||
| "$ErrorActionPreference = "Stop" | ||
| fnm env | Out-String | Invoke-Expression | ||
| fnm install 18 | ||
| $($__out__ = $(fnm exec --using=18 npm list -g --depth=0 | Select-String 'is-odd'); if ($__out__ -eq $null) { exit 1 } else { $__out__ })" | ||
| `; | ||
|
|
||
| exports[`PowerShell missing default-packages file does not error: PowerShell 1`] = ` | ||
| "$ErrorActionPreference = "Stop" | ||
| fnm env | Out-String | Invoke-Expression | ||
| fnm install 18" | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import fs from "fs" | ||
| import path from "path" | ||
| import { script } from "./shellcode/script.js" | ||
| import { Bash, PowerShell } from "./shellcode/shells.js" | ||
| import describe from "./describe.js" | ||
| import testTmpDir from "./shellcode/test-tmp-dir.js" | ||
|
|
||
| for (const shell of [Bash, PowerShell]) { | ||
| describe(shell, () => { | ||
| test(`installs default packages`, async () => { | ||
| const fnmDir = path.join(testTmpDir(), "fnm") | ||
| fs.mkdirSync(fnmDir, { recursive: true }) | ||
| fs.writeFileSync(path.join(fnmDir, "default-packages"), "is-odd\n") | ||
|
|
||
| await script(shell) | ||
| .then(shell.env({})) | ||
| .then(shell.call("fnm", ["install", "18"])) | ||
| .then( | ||
| shell.scriptOutputContains( | ||
| shell.call("fnm", [ | ||
| "exec", | ||
| "--using=18", | ||
| "npm", | ||
| "list", | ||
| "-g", | ||
| "--depth=0", | ||
| ]), | ||
| "'is-odd'" | ||
| ) | ||
| ) | ||
| .takeSnapshot(shell) | ||
| .execute(shell) | ||
| }) | ||
|
Comment on lines
+10
to
+46
|
||
|
|
||
| test(`missing default-packages file does not error`, async () => { | ||
| await script(shell) | ||
| .then(shell.env({})) | ||
| .then(shell.call("fnm", ["install", "18"])) | ||
| .takeSnapshot(shell) | ||
| .execute(shell) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -172,6 +172,8 @@ impl Command for Install { | |||||||||||
| enable_corepack(&version, config)?; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| install_default_packages(&version, config)?; | ||||||||||||
|
|
||||||||||||
| if use_installed { | ||||||||||||
| use_installed_version(&version, config)?; | ||||||||||||
| } | ||||||||||||
|
|
@@ -205,6 +207,55 @@ fn enable_corepack(version: &Version, config: &FnmConfig) -> Result<(), Error> { | |||||||||||
| Ok(()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| fn parse_default_packages_file(file_path: &std::path::Path) -> Result<Vec<String>, std::io::Error> { | ||||||||||||
| let contents = match std::fs::read_to_string(file_path) { | ||||||||||||
| Ok(contents) => contents, | ||||||||||||
| Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(vec![]), | ||||||||||||
| Err(err) => return Err(err), | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| Ok(contents | ||||||||||||
| .lines() | ||||||||||||
| .filter_map(|line| { | ||||||||||||
| let trimmed = line.trim(); | ||||||||||||
| if trimmed.is_empty() || trimmed.starts_with('#') { | ||||||||||||
| None | ||||||||||||
| } else { | ||||||||||||
| Some(trimmed.to_string()) | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| .collect()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| fn install_default_packages(version: &Version, config: &FnmConfig) -> Result<(), Error> { | ||||||||||||
| let packages = parse_default_packages_file(&config.default_packages_file())?; | ||||||||||||
| if packages.is_empty() { | ||||||||||||
| return Ok(()); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| let npm_path = version.installation_path(config); | ||||||||||||
| let npm_path = if cfg!(windows) { | ||||||||||||
| npm_path.join("npm.cmd") | ||||||||||||
| } else { | ||||||||||||
| npm_path.join("bin").join("npm") | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| let mut args: Vec<&str> = Vec::with_capacity(2 + packages.len()); | ||||||||||||
| args.push("install"); | ||||||||||||
| args.push("--global"); | ||||||||||||
| for package_spec in &packages { | ||||||||||||
| for arg in package_spec.split_whitespace() { | ||||||||||||
| args.push(arg); | ||||||||||||
| } | ||||||||||||
|
||||||||||||
| for arg in package_spec.split_whitespace() { | |
| args.push(arg); | |
| } | |
| // Treat each non-comment line from the default packages file as a single npm argument. | |
| args.push(package_spec.as_str()); |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
npm_path.to_str().unwrap() can panic if the installation path contains non-UTF8 bytes (possible on Unix). Prefer propagating an error (or using a lossy conversion) rather than panicking during fnm install.
| super::exec::Exec::new_for_version(version, npm_path.to_str().unwrap(), &args) | |
| let npm_path_str = npm_path.to_string_lossy(); | |
| super::exec::Exec::new_for_version(version, &npm_path_str, &args) |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DefaultPackagesError is marked transparent, so the user won’t get context that the failure happened while installing default packages. Consider giving this variant an explicit message (e.g., “Can't install default packages: …”) and optionally include the default-packages file path for easier debugging.
| #[error(transparent)] | |
| #[error("Can't install default packages: {source}")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs list
fnm install --reinstall-packages-from <version>, but theInstallcommand implementation insrc/commands/install.rsdoes not define this flag. Either implement the option or remove it from the generated command docs to avoid documenting a nonexistent CLI flag.