diff --git a/README.md b/README.md index 13ccd83..7929732 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,13 @@ The scripts cover both the frontend and the Go backend, so `make check` fails if either one is broken. We provide: - `script/bootstrap` — install all dependencies (the pinned node via nvm unless - one new enough for the frontend's dependencies is installed, yarn via - corepack, `yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on - `PATH` is exactly that version, and the Go modules), linking what it installs - itself into `~/.local/bin`, which has to be on `PATH`. It installs no Go - linter and not Docker: `make test` and `make lint` run in Docker + the `node` on `PATH` has the pinned major version and is new enough for the + frontend's dependencies, yarn `YARN_VERSION` via corepack unless the `yarn` on + `PATH` is exactly that version, `yarn install --frozen-lockfile`, Go + `GO_VERSION` unless the `go` on `PATH` is exactly that version, and the Go + modules), linking what it installs itself into `~/.local/bin`, which has to be + on `PATH`. It installs no Go linter and not Docker: `make test` and + `make lint` run in Docker - `script/setup` — make a fresh clone ready for development: bootstrap plus the git pre-commit hook - `script/dev` — run the Vite dev server, which proxies `/api` to a locally @@ -66,7 +68,9 @@ either one is broken. We provide: eslint in its `frontend-lint` stage, and golangci-lint over the Go packages - `script/fmt` — format all files (writes): prettier over the JavaScript, CSS, HTML and Markdown, then gofmt over `cmd/` and `internal/`. It runs on the - host, as `script/fmt-check` does, with `~/.local/bin` put on `PATH` + host, as `script/fmt-check` does, with `~/.local/bin` put on `PATH`, and runs + prettier under the pinned node in nvm when the `node` on `PATH` is not one + `script/bootstrap` would use - `script/fmt-check` — check formatting (read-only): prettier, then gofmt - `script/check` — run test, lint, and fmt-check - `script/add-dependency` — add a frontend package, or move one to another diff --git a/TODO.md b/TODO.md index 6fbeb9f..b51a92f 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,17 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-19: `script/bootstrap` keeps the `yarn` on `PATH` only when + `yarn --version` is exactly `YARN_VERSION`, and otherwise prepares that + version through corepack, as it did when no yarn was installed + ([#154](https://git.eeqj.de/sneak/netwatch/issues/154)); any yarn used to be + kept. It uses the `node` on `PATH` only when that node has the major version + of `NODE_VERSION` as well as being at least `NODE_MIN_VERSION`, as + `REPO_POLICIES.md` asks; a node 24 was kept before. After installing node or + yarn it looks the tool up through `PATH` again and stops if that is not the + pinned one, as it does for Go, and it prints the node and yarn versions it + uses. `script/fmt` and `script/fmt-check` use the same node test, and + otherwise run prettier under the pinned node in nvm - 2026-10-19: `DEBUG` unset or empty leaves debug logging off, and any other value, `false` and `0` included, turns it on, as the code style guide says ([#159](https://git.eeqj.de/sneak/netwatch/issues/159)). No value of it stops diff --git a/script/bootstrap b/script/bootstrap index 1bf0e7f..c235c09 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -3,11 +3,13 @@ # this repo. Idempotent: every install is guarded by a check so already # installed tools are skipped. Base tooling comes from nix, apt, brew, # or apk (detected in that order); assumes nothing is present. Node is -# used directly if it is at least NODE_MIN_VERSION; otherwise it is -# installed at a pinned version via nvm (installing nvm itself first, -# from a hash-verified release archive, never curl | sh). Go, with its -# gofmt, is used directly only if it is exactly GO_VERSION; otherwise -# that release is installed from its hash-verified archive. +# used directly only if it has the major version of NODE_VERSION and is +# at least NODE_MIN_VERSION; otherwise NODE_VERSION is installed via nvm +# (installing nvm itself first, from a hash-verified release archive, +# never curl | sh). Yarn is used directly only if it is exactly +# YARN_VERSION; otherwise that version is prepared through corepack. +# Go, with its gofmt, is used directly only if it is exactly GO_VERSION; +# otherwise that release is installed from its hash-verified archive. # # What this script installs outside the system package manager lives # under $HOME and is linked into ~/.local/bin, where make and the git @@ -142,32 +144,65 @@ ensure_nvm() { rm -rf "$tmp" } -# node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself -# compares the two: major, then minor, then patch. +# node_ok: the node on PATH has the major version of NODE_VERSION and is +# at least NODE_MIN_VERSION. Only the major is compared with the pin: +# the Dockerfile's node stages start from a node 22 alpine image whose +# exact version is not the pin, and nvm cannot install a prebuilt node +# on alpine. node itself compares the versions: major, then minor, then +# patch. A node that fails does not pass. script/bootstrap, script/fmt +# and script/fmt-check carry this function unchanged. node_ok() { - if missing node; then return 1; fi + if ! command -v node >/dev/null 2>&1; then return 1; fi node -e ' const have = process.versions.node.split(".").map(Number); const want = process.argv[1].split(".").map(Number); + const pinned = process.argv[2].split(".").map(Number); + if (have[0] !== pinned[0]) process.exit(1); for (let i = 0; i < 3; i++) { if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1); } - ' "$NODE_MIN_VERSION" + ' "$NODE_MIN_VERSION" "$NODE_VERSION" } # ensure_node: unless node_ok, install NODE_VERSION and link its node. +# Afterwards node is looked up through PATH again and must pass node_ok. ensure_node() { - if node_ok; then return 0; fi + if node_ok; then + echo "bootstrap: using node $(node --version)" + return 0 + fi ensure_nvm nvm_sh "nvm install $NODE_VERSION" link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node + hash -r + if ! node_ok; then + echo "bootstrap: after installing node $NODE_VERSION through nvm," >&2 + echo " the node on PATH, $(command -v node), is not it" >&2 + exit 1 + fi + echo "bootstrap: installed node $(node --version)" } -# ensure_yarn: corepack writes its shims (pnpm and yarnpkg as well as -# yarn) into $TOOLCHAIN rather than next to itself, and the npm fallback -# installs there too; only yarn is linked. +# yarn_ok: yarn --version on PATH prints exactly YARN_VERSION, compared +# over the whole version. A yarn that fails or prints anything else does +# not pass. +yarn_ok() { + if missing yarn; then return 1; fi + version="$(yarn --version 2>/dev/null)" || return 1 + [ "$version" = "$YARN_VERSION" ] +} + +# ensure_yarn: unless yarn_ok, prepare YARN_VERSION and link its yarn. +# corepack writes its shims (pnpm and yarnpkg as well as yarn) into +# $TOOLCHAIN rather than next to itself, and the npm fallback installs +# there too; only yarn is linked. Afterwards yarn is looked up through +# PATH again and must pass yarn_ok, so another yarn that hides the link +# stops bootstrap. ensure_yarn() { - if ! missing yarn; then return 0; fi + if yarn_ok; then + echo "bootstrap: using yarn $(yarn --version)" + return 0 + fi shims="$TOOLCHAIN/corepack-shims" mkdir -p "$shims" if ! missing corepack; then @@ -182,6 +217,13 @@ ensure_yarn() { shims="$TOOLCHAIN/npm-global/bin" fi link_bin "$shims/yarn" yarn + hash -r + if ! yarn_ok; then + echo "bootstrap: after installing yarn $YARN_VERSION in $shims," >&2 + echo " the yarn on PATH, $(command -v yarn), does not report it" >&2 + exit 1 + fi + echo "bootstrap: installed yarn $(yarn --version)" } # go_ok: the go on PATH has its gofmt beside it (a Go release ships the diff --git a/script/fmt b/script/fmt index f998d21..8603f68 100755 --- a/script/fmt +++ b/script/fmt @@ -8,12 +8,47 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# Must match the pins in script/bootstrap. +NODE_VERSION="22.17.0" +NODE_MIN_VERSION="22.13.0" + +# node_ok: the node on PATH has the major version of NODE_VERSION and is +# at least NODE_MIN_VERSION. Only the major is compared with the pin: +# the Dockerfile's node stages start from a node 22 alpine image whose +# exact version is not the pin, and nvm cannot install a prebuilt node +# on alpine. node itself compares the versions: major, then minor, then +# patch. A node that fails does not pass. script/bootstrap, script/fmt +# and script/fmt-check carry this function unchanged. +node_ok() { + if ! command -v node >/dev/null 2>&1; then return 1; fi + node -e ' + const have = process.versions.node.split(".").map(Number); + const want = process.argv[1].split(".").map(Number); + const pinned = process.argv[2].split(".").map(Number); + if (have[0] !== pinned[0]) process.exit(1); + for (let i = 0; i < 3; i++) { + if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1); + } + ' "$NODE_MIN_VERSION" "$NODE_VERSION" +} + main() { cd "$ROOT" # script/bootstrap links the node, yarn and gofmt it installs into # ~/.local/bin, which the shell that called it may not have on PATH. PATH="$HOME/.local/bin:$PATH" - "$ROOT/script/frontend-fmt" + # prettier runs under a node script/bootstrap would use: the node on + # PATH when it passes node_ok, and otherwise the pinned node in nvm. + # nvm is a bash script, hence the bash. + if node_ok; then + "$ROOT/script/frontend-fmt" + elif [ -s "$HOME/.nvm/nvm.sh" ]; then + bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && "$2"' \ + bash "$NODE_VERSION" "$ROOT/script/frontend-fmt" + else + echo "fmt: no node script/bootstrap would use; run it first" >&2 + exit 1 + fi gofmt -l -w cmd internal } diff --git a/script/fmt-check b/script/fmt-check index 91dd6b3..9f155c3 100755 --- a/script/fmt-check +++ b/script/fmt-check @@ -6,12 +6,47 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# Must match the pins in script/bootstrap. +NODE_VERSION="22.17.0" +NODE_MIN_VERSION="22.13.0" + +# node_ok: the node on PATH has the major version of NODE_VERSION and is +# at least NODE_MIN_VERSION. Only the major is compared with the pin: +# the Dockerfile's node stages start from a node 22 alpine image whose +# exact version is not the pin, and nvm cannot install a prebuilt node +# on alpine. node itself compares the versions: major, then minor, then +# patch. A node that fails does not pass. script/bootstrap, script/fmt +# and script/fmt-check carry this function unchanged. +node_ok() { + if ! command -v node >/dev/null 2>&1; then return 1; fi + node -e ' + const have = process.versions.node.split(".").map(Number); + const want = process.argv[1].split(".").map(Number); + const pinned = process.argv[2].split(".").map(Number); + if (have[0] !== pinned[0]) process.exit(1); + for (let i = 0; i < 3; i++) { + if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1); + } + ' "$NODE_MIN_VERSION" "$NODE_VERSION" +} + main() { cd "$ROOT" # script/bootstrap links the node, yarn and gofmt it installs into # ~/.local/bin, which the shell that called it may not have on PATH. PATH="$HOME/.local/bin:$PATH" - "$ROOT/script/frontend-fmt-check" + # prettier runs under a node script/bootstrap would use: the node on + # PATH when it passes node_ok, and otherwise the pinned node in nvm. + # nvm is a bash script, hence the bash. + if node_ok; then + "$ROOT/script/frontend-fmt-check" + elif [ -s "$HOME/.nvm/nvm.sh" ]; then + bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && "$2"' \ + bash "$NODE_VERSION" "$ROOT/script/frontend-fmt-check" + else + echo "fmt-check: no node script/bootstrap would use; run it first" >&2 + exit 1 + fi unformatted="$(gofmt -l cmd internal)" if [ -n "$unformatted" ]; then echo "Go files not formatted:" >&2