diff --git a/.github/ISSUE_TEMPLATE/ai_content_correction.md b/.github/ISSUE_TEMPLATE/ai_content_correction.md new file mode 100644 index 0000000..983fedb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ai_content_correction.md @@ -0,0 +1,27 @@ +--- +name: AI content correction +about: Report inaccurate, outdated, or hallucinated info in AI-generated docs (e.g. LEARN_GO_AND_NODEUP.md) or AI-authored PR/issue content +title: 'docs(ai): ' +labels: ['documentation'] +assignees: [] +--- + +## Where is the problem? + +File and section/line (e.g. `LEARN_GO_AND_NODEUP.md`, section 4 +"Functions, multiple return values...", or a specific PR/issue/comment +authored by an AI assistant). + +## What does it currently say? + +Quote or paraphrase the claim. + +## What's wrong with it? + +Is it factually incorrect, outdated (the code changed since it was +written), misleading, or just confusing? A code snippet or line number +that contradicts the claim is the most useful thing you can include. + +## Suggested correction (optional) + +If you already know what it should say instead, note it here. diff --git a/LEARN_GO_AND_NODEUP.md b/LEARN_GO_AND_NODEUP.md new file mode 100644 index 0000000..8472b65 --- /dev/null +++ b/LEARN_GO_AND_NODEUP.md @@ -0,0 +1,397 @@ +# Learning Go Through `nodeup` — A Guide From Zero + +This document teaches Go to someone who has never written a line of it, +using a real, working project (`nodeup`) instead of toy examples. Every +concept below is paired with an actual line from this codebase. It's meant +for anyone approaching `nodeup` with zero Go background — a new +contributor, a reviewer, or the maintainer — read it top to bottom once, +then keep it as a reference while you read the actual source. + +> **A note on how this document was made:** this guide, and the companion +> `REVIEW_FINDINGS.md` in this repo, were generated by an AI assistant +> (Claude) reading the codebase. That means two things worth keeping in +> mind: +> +> 1. It's a genuinely useful starting map, cross-checked against the real +> source at the time it was written — not a generic Go tutorial with +> the project's name pasted in. +> 2. It can still be wrong. AI-generated explanations can misstate how +> something works, go stale as the code changes, or occasionally just +> hallucinate a detail that sounds plausible but isn't true. Treat every +> claim here as "probably right, cross-check against the actual file +> and line referenced" rather than gospel. +> +> If you spot something here that's inaccurate, outdated, or just doesn't +> match what the code actually does, please file it — use the **"AI +> content correction"** issue template +> (`.github/ISSUE_TEMPLATE/ai_content_correction.md`) so mistakes get +> tracked and fixed rather than quietly misleading the next reader. + +--- + +## 1. The big picture: what even is Go, and why does this project use it + +Go (a.k.a. Golang) is a compiled, statically-typed language made by Google. +"Compiled" means `.go` source files get turned into a single standalone +executable file (a "binary") — unlike Node.js or Python, there's no +interpreter needed on the end user's machine. That's *why* `nodeup` is +written in Go: it's a tool that manages Node.js installs, so it can't +require Node.js to already be working correctly just to run itself (the +"bootstrap paradox"). + +"Statically typed" means every variable's type (string, number, etc.) is +known at compile time, not figured out while the program runs. This is +different from JavaScript, where a variable can hold a string one moment +and a number the next. In Go, once something is declared a `string`, it's a +`string` forever. + +## 2. Reading a real file, line by line + +Open `internal/node/dist.go` (or just read the excerpt below — it's real +code from this project) and let's go through it. + +```go +// Package node provides access to the nodejs.org/dist/index.json API. +// It fetches, caches, and resolves Node.js versions (LTS, Current, etc.). +package node + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/Masterminds/semver/v3" +) +``` + +- **`package node`** — every Go file belongs to a "package," which is + Go's word for a module/namespace. All files in the same folder + (`internal/node/`) must declare the same package name. This is + roughly like a Python module or a JS file that exports things — except + in Go, *the folder* is the unit, not the file. +- **`import (...)`** — like `require()` in Node.js or `import` in + JavaScript. Each line is a package path. `"encoding/json"`, `"fmt"`, + `"io"`, `"net/http"`, `"os"`, `"path/filepath"`, `"time"` are all part + of Go's **standard library** — they ship with Go itself, no npm-style + install needed. `"github.com/Masterminds/semver/v3"` is a **third-party + dependency** — the equivalent of something in `package.json`, except + Go's version is `go.mod` (more on that in section 6). + +### Comments as documentation + +Notice the `// Package node provides...` comment directly above `package +node`. In Go, a comment immediately above a declaration (a package, a +function, a type) *is* that thing's official documentation — tools like +`go doc` extract it automatically. This is why this project's CLAUDE.md +says "no exported names without a godoc comment" — every public +function/type is expected to have one of these directly above it. + +## 3. Types: structs, and the concept of a "shape" + +```go +type ManifestVersion struct { + Version string `json:"version"` + Date string `json:"date"` + LTSCodename *string `json:"lts"` +} +``` + +- **`type ... struct { ... }`** defines a new named "shape" — think of it + like a TypeScript `interface` or a Python `dataclass`, except Go doesn't + have classes at all (see section 5). +- Each line inside is a **field**: a name, then its type. `Version string` + means "a field called Version, which holds text." No `:` like + TypeScript, no `=` — just name then type, separated by whitespace. +- **`` `json:"version"` ``** is a *struct tag* — metadata attached to a + field, read by the `encoding/json` package at runtime. It tells Go's + JSON decoder "when you see a JSON key called `version`, put its value + into this field." This is how `dist.go` turns nodejs.org's raw JSON + response into a usable Go value. +- **`*string`** — the `*` means "pointer to a string," which in Go's + idiom here really means "either nil (absent), or a string." Go doesn't + have `null`/`undefined` the way JS does; a plain `string` can never be + "missing," it defaults to `""` (empty string) if unset. To represent + "this field might genuinely be absent," Go code uses a pointer, because + a pointer's zero-value is `nil` (Go's version of "nothing here"). + This exact trick is why `LTSCodename` is `*string`: nodejs.org's `lts` + JSON field is either `false` (not LTS) or a string codename like + `"Iron"` (is LTS) — `nil` means "not LTS," a non-nil pointer means "LTS, + and here's its codename." + +## 4. Functions, multiple return values, and Go's approach to errors + +This is the single biggest mental shift coming from JS/Python. Look at this +real function: + +```go +func (m Manifest) LatestLTS() (*ManifestVersion, error) { + var latest *ManifestVersion + var latestSem *semver.Version + + for i := range m { + if m[i].LTSCodename == nil { + continue + } + v, err := parseVersion(m[i].Version) + if err != nil { + continue + } + if latestSem == nil || v.GreaterThan(latestSem) { + latestSem = v + latest = &m[i] + } + } + + if latest == nil { + return nil, fmt.Errorf("no LTS version found in manifest") + } + return latest, nil +} +``` + +- **`func (m Manifest) LatestLTS() (*ManifestVersion, error)`** — this + declares a function called `LatestLTS` that is attached to the + `Manifest` type (the `(m Manifest)` part is called a *receiver* — it's + Go's substitute for "methods on a class"; more in section 5). It + returns **two values**: a `*ManifestVersion` (the result, or nil if + none found) and an `error` (nil if everything went fine, non-nil if + something went wrong). +- **This two-value return is Go's entire error-handling philosophy.** + There are no `try`/`catch` blocks for normal error flow in Go (there IS + a `panic`/`recover` mechanism, but it's reserved for truly exceptional, + unrecoverable situations — not everyday error handling). Instead, every + function that can fail returns an `error` as its *last* return value, + and the caller is expected to immediately check it: + + ```go + v, err := parseVersion(m[i].Version) + if err != nil { + continue + } + ``` + + Read this as: "try to parse the version; if that produced an error, + skip this entry and move to the next one." This `if err != nil { ... + }` pattern appears *everywhere* in Go code — you'll see it dozens of + times per file. It's not boilerplate to be annoyed by; it's the + language's core discipline: **you cannot use a value without + acknowledging whether it succeeded.** +- This is exactly why this project's CLAUDE.md says *"errcheck is enabled + and treated as a bug. Every error return must be handled."* The linter + `errcheck` scans the code for any place a function returns an error that + then never gets looked at — which in Go is very easy to do by accident + (unlike JS, an unhandled error doesn't throw; it just sits there in a + variable nobody checked). +- **`for i := range m`** — a loop. `range` over a slice (Go's dynamic + array, see section 5) gives you the index `i` each time through. This is + like `for (let i = 0; i < m.length; i++)` in JS, just shorter. +- **`:=`** is Go's "declare and assign" operator. `v, err := parseVersion(...)` + means "create two new variables, `v` and `err`, and assign them the two + return values from this call." Compare to `var latest *ManifestVersion` + earlier, which declares a variable *without* assigning it yet (it + starts at its zero value — `nil` for a pointer). +- **`&m[i]`** — the `&` operator means "give me a pointer to this value" + (the address-of operator). `latest = &m[i]` stores a pointer to the + i-th element of the manifest, not a copy of it. + +### Error wrapping + +Elsewhere in the same file: + +```go +return nil, fmt.Errorf("fetch manifest from %s: %w", url, err) +``` + +`fmt.Errorf` is like a `sprintf` for building error messages. The special +verb **`%w`** ("wrap") embeds the *original* error inside the new one, +preserving the full chain so you can later ask "was this ultimately caused +by a network timeout?" without losing the outer context ("fetch manifest +from https://...: dial tcp: i/o timeout"). This wrap-with-context pattern +is idiomatic Go and shows up throughout `internal/`. + +## 5. No classes — structs + methods + interfaces instead + +Go has no `class` keyword. Instead, it composes three things to get +class-like behavior: + +**A struct** (the data): +```go +type Config struct { + Manager string + Track TrackConfig +} +``` + +**A method** (a function "attached" to that struct via a receiver): +```go +func (c *Config) IsValid() bool { + ... +} +``` +Read `(c *Config)` as "this function can be called as `myConfig.IsValid()`, +and inside the function, `c` refers to that specific config." The `*` means +it receives a *pointer* to the struct (so it can see/modify the real one), +not a copy. + +**An interface** (a contract — "any type that has these methods counts"): +```go +// internal/detector/detector.go (conceptually) +type Manager interface { + Name() string + Detect() bool + Version() (string, error) + ListInstalled() ([]semver.Version, error) +} +``` +This is the most important interface in the whole codebase. It says: +"anything that has a `Name()` method returning a string, a `Detect()` +method returning bool, etc., **counts as** a `Manager` — it doesn't matter +what struct it actually is." That's how `fnm.go`, `nvm.go`, `volta.go`, and +five other files can all be treated identically by `detector.go`'s +`DetectAll()` function, even though they're completely separate types with +totally different internals (`nvm` shells out to bash, `fnm` calls a real +binary directly). This is Go's version of polymorphism — instead of "class +Fnm extends Manager," Go says "anything shaped like a Manager IS a +Manager," checked automatically by the compiler. This is called +**structural typing** or "duck typing, but checked at compile time." + +## 6. Packages, modules, and this project's folder structure + +- **`go.mod`** (at the repo root) is the Go equivalent of `package.json`. + Open it — you'll see: + ``` + module github.com/dipto0321/nodeup + go 1.24.0 + require ( + github.com/Masterminds/semver/v3 v3.5.0 + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 + ) + ``` + `module github.com/dipto0321/nodeup` names the whole project (Go uses + the eventual GitHub URL as the name — that's just Go's convention for + making import paths globally unique). The `require` block lists the + actual dependencies — Cobra (CLI framework), Masterminds/semver (version + comparison), yaml.v3 (config file parsing). Three total. That's it — the + project's "no new dependencies without a rationale line" rule is easy to + honor when the list is this short. +- **`go.sum`** is like `package-lock.json` — exact cryptographic hashes of + every dependency version, so builds are reproducible and tamper-evident. + Never hand-edit it. +- **`internal/`** is a folder name Go treats specially: any package inside + a folder called `internal/` can **only** be imported by code that lives + in the same repo, at or above that `internal/`'s parent directory. This + is enforced by the Go compiler itself, not a convention — try to `import + "github.com/dipto0321/nodeup/internal/config"` from a different project + and it simply won't compile. That's why CLAUDE.md notes "`internal/` + packages cannot be imported by external projects (Go enforces this)" — + it's a real language feature, used here to say "everything under + `internal/` is nodeup's own business logic, not a public library." +- **`cmd/nodeup/main.go`** is the entrypoint. By Go convention, the + `cmd//` folder pattern is where you put the tiny file that + just wires everything else together and calls `main()` — Go requires + exactly one function named `main()` in a package named `main` as the + program's starting point, the same way C requires `int main()`. + +## 7. Concurrency vocabulary you'll see (even if unused yet) + +Go is famous for **goroutines** (`go someFunc()` — runs a function +concurrently) and **channels** (`chan int` — a pipe for passing values +between goroutines safely). Skimming this codebase, `nodeup` doesn't +currently use either — every operation is sequential (detect, then fetch, +then snapshot, then install...). You may see this vocabulary in library +code the project depends on (Cobra, the HTTP client) even without ever +writing a `go func(){}()` yourself. If a future feature parallelizes, say, +fetching data for multiple managers at once, this is the mechanism that +would be used — for now, it's safe to ignore. + +## 8. Testing in Go + +Go's test framework is built into the standard library — no Jest/Mocha +needed. A file named `*_test.go` (e.g. `internal/node/dist_test.go`) sits +next to the code it tests, and each test is just a function starting with +`Test`: + +```go +func TestLatestLTS(t *testing.T) { + ... + if got.Version != "v20.15.0" { + t.Errorf("expected v20.15.0, got %s", got.Version) + } +} +``` + +`t *testing.T` is a handle the test uses to report failures — `t.Errorf` +marks the test failed but keeps running (compare to `t.Fatalf`, which +stops immediately). Run all tests with `go test ./...` (the `...` means +"this package and every package below it") — this is exactly what `make +test` runs, plus `-race` (detects concurrency bugs) and +`-coverprofile=coverage.out` (measures what % of lines the tests exercise). + +## 9. The specific tools enforcing this project's rules + +You don't need to memorize these, but it helps to know what they're for +when CI fails: + +| Tool | What it does | Why nodeup uses it | +|---|---|---| +| `go build` | Compiles the code into a binary | Catches type errors, missing imports | +| `go vet` | Catches suspicious-but-technically-legal code | e.g. a format string that doesn't match its arguments | +| `golangci-lint` | Runs ~15 linters at once (a "linter of linters") | Enforces the errcheck/contextcheck/gocritic rules CLAUDE.md mentions | +| `go test -race` | Runs the tests, watching for data races | Catches two goroutines touching the same memory unsafely | +| `gofmt` (used by golangci-lint) | Auto-formats code to one canonical style | So diffs are never about whitespace | + +`errcheck` (mentioned throughout CLAUDE.md) is one linter *inside* +golangci-lint specifically dedicated to "was an error return value ever +looked at." `contextcheck` is another, dedicated to "is +`context.Background()` being used where an existing context should be" — +this came up repeatedly in the code review that produced `REVIEW_FINDINGS.md`, +so it's worth understanding *why* it matters: + +### What is a `context.Context`, really? + +A `Context` is Go's way of carrying a "this operation might get cancelled" +signal through a chain of function calls. When a user presses Ctrl-C on a +running `nodeup upgrade`, or when a future timeout fires, that signal needs +to reach all the way down into whatever subprocess (`npm install -g ...`) +is currently running, so it can be killed instead of running to +completion. Cobra (the CLI framework used here) gives every command a +`cmd.Context()` that's wired to exactly this kind of cancellation. When +code instead calls `context.Background()` (a context that can *never* be +cancelled — it's a placeholder for "no context available"), it silently +breaks that chain: Ctrl-C stops responding for that operation. This is +why several review findings in `REVIEW_FINDINGS.md` are about spotting +`context.Background()` where `cmd.Context()` should have been threaded +through instead. + +## 10. How to actually read the rest of the codebase now + +A practical reading order, using what you now know: + +1. Start at `cmd/nodeup/main.go` — the entrypoint, tiny. +2. Go to `internal/cli/root.go` — see how subcommands register themselves. +3. Pick one subcommand file, e.g. `internal/cli/version.go` — it's the + simplest, good for confirming you can follow a `RunE` function + end-to-end. +4. Then `internal/detector/detector.go` — the `Manager` interface, the + core abstraction everything else builds on. +5. Then whichever `internal/detector/.go` file matches a version + manager you personally use (e.g. `fnm.go` if you use fnm) — seeing + real, concrete code that talks to a tool you already understand + operationally will cement the patterns fastest. + +Every time you hit an unfamiliar piece of syntax, search this document +first — most of Go's "new-looking" syntax is one of: a struct tag, a +pointer, a multiple-return-with-error, a receiver method, or an interface. +There genuinely isn't much more to the language than that plus the +standard library. + +--- + +*Found something in this document that's wrong, stale, or doesn't match +the actual code? Please file it using the "AI content correction" issue +template — see `.github/ISSUE_TEMPLATE/ai_content_correction.md`.*