Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/serious-shoes-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fnm": patch
---

Add generated man pages for `fnm` and subcommands, and keep them in sync through CI and release prep checks.
5 changes: 3 additions & 2 deletions .ci/prepare-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const command = cmd.command({
updateCargoToml(await getPackageVersion())
exec("cargo build --release")
exec("pnpm generate-command-docs --binary-path=./target/release/fnm")
exec("pnpm generate-man-page --binary-path=./target/release/fnm")
exec("./.ci/record_screen.sh")
},
})
Expand All @@ -34,7 +35,7 @@ cmd.run(cmd.binary(command), process.argv)
async function getPackageVersion() {
const pkgJson = await fs.promises.readFile(
new URL("../package.json", import.meta.url),
"utf8"
"utf8",
)
const version = JSON.parse(pkgJson).version
assert(version, "package.json version is not set")
Expand All @@ -48,7 +49,7 @@ function updateCargoToml(nextVersion) {

const newToml = cargoToml.replace(
`version = "${currentVersion}"`,
`version = "${nextVersion}"`
`version = "${nextVersion}"`,
)

if (newToml === cargoToml) {
Expand Down
167 changes: 167 additions & 0 deletions .ci/print-man-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env node

/// @ts-check

import { execa } from "execa"
import fs from "node:fs"
import cmd from "cmd-ts"
import cmdFs from "cmd-ts/dist/cjs/batteries/fs.js"

const FnmBinaryPath = {
...cmdFs.ExistingPath,
defaultValue() {
const target = new URL("../target/debug/fnm", import.meta.url)
if (!fs.existsSync(target)) {
throw new Error(
"Can't find debug target, please run `cargo build` or provide a specific binary path",
)
}
return target.pathname
},
}

const command = cmd.command({
name: "print-man-page",
description: "prints the man/*.1 files with updated contents",
args: {
checkForDirty: cmd.flag({
long: "check",
description: `Check that file was not changed`,
}),
fnmPath: cmd.option({
long: "binary-path",
description: "the fnm binary path",
type: FnmBinaryPath,
}),
},
async handler({ checkForDirty, fnmPath }) {
const targetFiles = await main(fnmPath)
if (checkForDirty) {
const gitStatus = await checkGitStatus(targetFiles)
if (gitStatus.state === "dirty") {
process.exitCode = 1
console.error(
"The files have changed. Please re-run `pnpm generate-man-page`.",
)
console.error(`hint: The following diff was found:`)
console.error()
console.error(gitStatus.diff)
}
}
},
})

cmd.run(cmd.binary(command), process.argv).catch((err) => {
console.error(err)
process.exitCode = process.exitCode || 1
})

/**
* @param {string} fnmPath
* @returns {Promise<string[]>}
*/
async function main(fnmPath) {
const manDir = new URL("../man/", import.meta.url).pathname
await fs.promises.mkdir(manDir, { recursive: true })
const subcommands = await getSubcommands(fnmPath)
const targets = [
{
path: `${manDir}fnm.1`,
args: ["man"],
},
...subcommands.map((name) => ({
path: `${manDir}fnm-${name}.1`,
args: ["man", name],
})),
]

for (const target of targets) {
await writeManPage(fnmPath, target.path, target.args)
}

return targets.map((target) => target.path)
}

/**
* @param {string} fnmPath
* @param {string} targetFile
* @param {string[]} args
* @returns {Promise<void>}
*/
async function writeManPage(fnmPath, targetFile, args) {
const result = await execa(fnmPath, args, {
reject: false,
stdout: "pipe",
stderr: "pipe",
})

if (result.exitCode !== 0) {
throw new Error(result.stderr || "Failed generating man page")
}

await fs.promises.writeFile(targetFile, result.stdout, "utf8")
}

/**
* @param {string} fnmPath
* @returns {Promise<string[]>}
*/
async function getSubcommands(fnmPath) {
const result = await execa(fnmPath, ["--help"], {
reject: false,
stdout: "pipe",
stderr: "pipe",
})

if (result.exitCode !== 0) {
throw new Error(result.stderr || "Failed reading fnm --help")
}

const rows = result.stdout.split("\n")
const commandsHeader = rows.findIndex((line) => line.trim() === "Commands:")

if (commandsHeader === -1) {
return []
}

const end = rows.indexOf("", commandsHeader + 1)
const commandRows = rows.slice(
commandsHeader + 1,
end === -1 ? undefined : end,
)

/** @type {string[]} */
const subcommands = []

for (const row of commandRows) {
const [name] = row.trim().split(/\s+/)
if (!name) {
continue
}
subcommands.push(name)
}

return subcommands
}

/**
* @param {string[]} targetFiles
* @returns {Promise<{ state: "dirty", diff: string } | { state: "clean" }>}
*/
async function checkGitStatus(targetFiles) {
if (targetFiles.length === 0) {
return { state: "clean" }
}

const { stdout, exitCode } = await execa(
`git`,
["diff", "--color", "--exit-code", ...targetFiles],
{
reject: false,
},
)
if (exitCode === 0) {
return { state: "clean" }
}
return { state: "dirty", diff: stdout }
}
1 change: 1 addition & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ jobs:
- name: Generate command markdown
run: |
pnpm run generate-command-docs --check --binary-path=$(which fnm)
pnpm run generate-man-page --check --binary-path=$(which fnm)

# TODO: use bnz
# run_e2e_benchmarks:
Expand Down
19 changes: 18 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ url = "2.5.0"
sysinfo = "0.30.12"
thiserror = "1.0.61"
clap_complete = "4.5.2"
clap_mangen = "0.2.24"
anyhow = "1.0.86"
indicatif = { version = "0.17.8", features = ["improved_unicode"] }
flate2 = "1.0.30"
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ Where `<SHELL>` can be one of the supported shells:

Please follow your shell instructions to install them.

## Man page

`fnm` ships generated man pages under `man/*.1` (`fnm.1` and command-specific pages such as `fnm-install.1`).

You can open it directly from the repository:

```sh
man ./man/fnm.1
```

Or install all pages under your manpath, for example:

```sh
mkdir -p "$HOME/.local/share/man/man1"
cp ./man/*.1 "$HOME/.local/share/man/man1/"
man fnm
man fnm-install
```

If you install fnm via a package manager (for example Homebrew), the man page may already be installed for you.

### Shell Setup

Environment variables need to be setup before you can start using fnm.
Expand Down
84 changes: 84 additions & 0 deletions man/fnm-alias.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH fnm-alias 1 "alias "
.SH NAME
fnm\-alias \- Alias a version to a common name
.SH SYNOPSIS
\fBalias\fR [\fB\-\-node\-dist\-mirror\fR] [\fB\-\-fnm\-dir\fR] [\fB\-\-log\-level\fR] [\fB\-\-arch\fR] [\fB\-\-version\-file\-strategy\fR] [\fB\-\-corepack\-enabled\fR] [\fB\-\-resolve\-engines\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fITO_VERSION\fR> <\fINAME\fR>
.SH DESCRIPTION
Alias a version to a common name
.SH OPTIONS
.TP
\fB\-\-node\-dist\-mirror\fR \fI<NODE_DIST_MIRROR>\fR [default: https://nodejs.org/dist]
<https://nodejs.org/dist/> mirror
.RS
May also be specified with the \fBFNM_NODE_DIST_MIRROR\fR environment variable.
.RE
.TP
\fB\-\-fnm\-dir\fR \fI<BASE_DIR>\fR
The root directory of fnm installations
.RS
May also be specified with the \fBFNM_DIR\fR environment variable.
.RE
.TP
\fB\-\-log\-level\fR \fI<LOG_LEVEL>\fR [default: info]
The log level of fnm commands
.br

.br
[\fIpossible values: \fRquiet, error, info]
.RS
May also be specified with the \fBFNM_LOGLEVEL\fR environment variable.
.RE
.TP
\fB\-\-arch\fR \fI<ARCH>\fR
Override the architecture of the installed Node binary. Defaults to arch of fnm binary
.RS
May also be specified with the \fBFNM_ARCH\fR environment variable.
.RE
.TP
\fB\-\-version\-file\-strategy\fR \fI<VERSION_FILE_STRATEGY>\fR [default: local]
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
.br

.br
\fIPossible values:\fR
.RS 14
.IP \(bu 2
local: Use the local version of Node defined within the current directory
.IP \(bu 2
recursive: Use the version of Node defined within the current directory and all parent directories
.RE
.RS
May also be specified with the \fBFNM_VERSION_FILE_STRATEGY\fR environment variable.
.RE
.TP
\fB\-\-corepack\-enabled\fR
Enable corepack support for each new installation. This will make fnm call `corepack enable` on every Node.js installation. For more information about corepack see <https://nodejs.org/api/corepack.html>
.RS
May also be specified with the \fBFNM_COREPACK_ENABLED\fR environment variable.
.RE
.TP
\fB\-\-resolve\-engines\fR [\fI<RESOLVE_ENGINES>\fR]
Resolve `engines.node` field in `package.json` whenever a `.node\-version` or `.nvmrc` file is not present.
This feature is enabled by default. To disable it, provide `\-\-resolve\-engines=false`.

Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
Note 2: If you disable it, please open an issue on GitHub describing _why_ you disabled it.
In the future, disabling it might be a no\-op, so it\*(Aqs worth knowing any reason to
do that.
.br

.br
[\fIpossible values: \fRtrue, false]
.RS
May also be specified with the \fBFNM_RESOLVE_ENGINES\fR environment variable.
.RE
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help (see a summary with \*(Aq\-h\*(Aq)
.TP
<\fITO_VERSION\fR>

.TP
<\fINAME\fR>
Loading
Loading