diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..23d67fc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules/ +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8af31cd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "proseWrap": "always" +} diff --git a/README.md b/README.md index f83d065..3636360 100644 --- a/README.md +++ b/README.md @@ -957,7 +957,9 @@ development workflow, and the Makefile targets are thin shims that call them. We provide: * `script/bootstrap` — install all development dependencies (Go, Go - module download). A host without Go gets the `go.mod` version through + module download, and node, yarn and `prettier` for Markdown). Node + comes from the host, or else at a pinned version through `nvm`. A + host without Go gets the `go.mod` version through `script/install-go`, in `.tool/go`, which `script/bootstrap` itself, the `Makefile`, `script/fmt`, `script/fmt-check`, `script/precommit` and `script/release` add to their `PATH`. It @@ -1011,9 +1013,11 @@ them. We provide: needs a *local* daemon. It is a developer convenience and never a gate: no gate reads its exit status. Run `make lint` afterwards to find out whether the tree is clean. -* `script/fmt` — format all code (writes) +* `script/fmt` — format all code (writes): Go with `go fmt`, Markdown + with `prettier` * `script/fmt-check` — check formatting (read-only). It runs `gofmt` on - the host, over every Go file outside `.tool`. + the host, over every Go file outside `.tool`, and `prettier --check` + over every Markdown file. * `script/check` — run `script/test`, `script/lint`, and `script/fmt-check`. * `script/docker` — build the Docker image tagged via diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc05cde --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "prettier": "3.8.1" + } +} diff --git a/script/bootstrap b/script/bootstrap index c1024a0..db6dd65 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -3,11 +3,20 @@ # 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 (not git, -# make, or go). +# make, or go). Node is used directly if installed; otherwise it is +# installed at a pinned version via nvm (installing nvm itself first, +# from a hash-verified release archive, never curl | sh). set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# Pinned versions, 2026-07-06 +NODE_VERSION="22.17.0" +NVM_VERSION="0.40.3" +# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz +NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0" +YARN_VERSION="1.22.22" + PKGMGR="" SUDO="" @@ -48,6 +57,69 @@ missing() { ! command -v "$1" >/dev/null 2>&1 } +# verify_sha256 +verify_sha256() { + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$1" | cut -d' ' -f1)" + else + actual="$(shasum -a 256 "$1" | cut -d' ' -f1)" + fi + if [ "$actual" != "$2" ]; then + echo "bootstrap: sha256 mismatch for $1" >&2 + echo " expected: $2" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +# nvm is a bash script; run a command in a bash with nvm loaded +nvm_sh() { + bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*" +} + +ensure_nvm() { + [ -s "$HOME/.nvm/nvm.sh" ] && return 0 + # nvm prerequisites; nvm itself requires bash + if missing bash; then pkg_install bash bash bash bash; fi + if missing curl; then pkg_install curl curl curl curl; fi + if missing git; then pkg_install git git git git; fi + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/nvm.tar.gz" \ + "https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz" + verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256" + mkdir -p "$HOME/.nvm" + tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1 + rm -rf "$tmp" +} + +ensure_node() { + if ! missing node; then return 0; fi + ensure_nvm + nvm_sh "nvm install $NODE_VERSION" +} + +ensure_yarn() { + if ! missing yarn; then return 0; fi + if ! missing corepack; then + corepack enable + corepack prepare "yarn@$YARN_VERSION" --activate + elif [ -s "$HOME/.nvm/nvm.sh" ]; then + nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \ + corepack prepare yarn@$YARN_VERSION --activate" + else + npm install -g "yarn@$YARN_VERSION" + fi +} + +install_js_deps() { + if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then + nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \ + yarn install --frozen-lockfile" + else + yarn install --frozen-lockfile + fi +} + # Docker is a hard requirement, not a nice-to-have: script/lint and # script/test build the lint and test phases of the Dockerfile, the only # place the linter and the tests run, and script/check and @@ -103,6 +175,8 @@ main() { # Base tooling (every repo) if missing git; then pkg_install git git git git; fi if missing make; then pkg_install gnumake make make make; fi + # script/install-go, script/install-goreleaser and nvm download with it. + if missing curl; then pkg_install curl curl curl curl; fi # Go toolchain: the host's own, or else the version go.mod names, # hash-verified, in .tool/go. That directory is not on the caller's @@ -131,6 +205,11 @@ main() { go mod download + # prettier, which script/fmt and script/fmt-check run on Markdown. + ensure_node + ensure_yarn + install_js_deps + # Last, so that everything installable is installed before the one # thing this script cannot install decides the outcome. require_docker diff --git a/script/fmt b/script/fmt index f4876e1..1f7ad98 100755 --- a/script/fmt +++ b/script/fmt @@ -4,11 +4,31 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# Must match the pin in script/bootstrap. +NODE_VERSION="22.17.0" + +# script/bootstrap installs node and yarn under nvm and leaves neither +# on the PATH of the shell that called it, so resolve the pinned +# toolchain here the way bootstrap's own install step does. nvm is a +# bash script, hence the subshell. +run_yarn() { + if command -v yarn >/dev/null 2>&1; then + exec yarn "$@" + fi + if [ ! -s "$HOME/.nvm/nvm.sh" ]; then + echo "fmt: no yarn; run script/bootstrap first" >&2 + exit 1 + fi + exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && + shift && exec yarn "$@"' bash "$NODE_VERSION" "$@" +} + main() { cd "$ROOT" # Where script/bootstrap installs Go when the host has none. PATH="$PATH:$ROOT/.tool/go/bin" go fmt ./... + run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always } main "$@" diff --git a/script/fmt-check b/script/fmt-check index 9a9a333..d0595ba 100755 --- a/script/fmt-check +++ b/script/fmt-check @@ -3,11 +3,30 @@ # writing. It checks every Go file outside .tool, which is more than # script/fmt formats: `go fmt ./...` skips `testdata` directories and # files and directories whose names start with `.` or `_`. Fix a file -# only this reports with `gofmt -w`. +# only this reports with `gofmt -w`. Markdown is checked with prettier. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# Must match the pin in script/bootstrap. +NODE_VERSION="22.17.0" + +# script/bootstrap installs node and yarn under nvm and leaves neither +# on the PATH of the shell that called it, so resolve the pinned +# toolchain here the way bootstrap's own install step does. nvm is a +# bash script, hence the subshell. +run_yarn() { + if command -v yarn >/dev/null 2>&1; then + exec yarn "$@" + fi + if [ ! -s "$HOME/.nvm/nvm.sh" ]; then + echo "fmt-check: no yarn; run script/bootstrap first" >&2 + exit 1 + fi + exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && + shift && exec yarn "$@"' bash "$NODE_VERSION" "$@" +} + main() { cd "$ROOT" # Where script/bootstrap installs Go when the host has none. @@ -23,6 +42,7 @@ main() { echo "$unformatted" >&2 exit 1 fi + run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always } main "$@" diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d846639 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +prettier@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==