diff --git a/.golangci.yml b/.golangci.yml index d0cca7e1e..ac2668c57 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,6 +22,7 @@ linters: - vendor - dist - site + - node_modules rules: # Test files use literal strings for clarity - linters: [goconst] diff --git a/go.mod b/go.mod index e175f67d3..fb54c7a04 100644 --- a/go.mod +++ b/go.mod @@ -34,3 +34,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/protobuf v1.36.12 // indirect ) + +ignore node_modules diff --git a/internal/compliance/go_mod_ignore_test.go b/internal/compliance/go_mod_ignore_test.go new file mode 100644 index 000000000..94a5249b0 --- /dev/null +++ b/internal/compliance/go_mod_ignore_test.go @@ -0,0 +1,58 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +package compliance + +import ( + "encoding/json" + "os/exec" + "testing" +) + +// TestGoModIgnoresNodeModules verifies that the root go.mod +// carries an `ignore node_modules` directive. +// +// npm packages may ship Go sources (flatted does, under +// flatted/golang/). Without the directive, `./...` matches +// them as packages of this module, so `go build`, `go vet`, +// `go test`, and golangci-lint all compile and lint +// third-party code the moment anyone runs `npm install` in +// editors/vscode, ctx-desktop, or tools/typecheck/*. The +// bare form (no `./` prefix) matches a node_modules +// directory at any depth. +// +// The directive is read through `go mod edit -json` rather +// than by string search, so an ignore block or a reformat +// cannot fool the check. +// +// See specs/go-ignore-node-modules.md. +func TestGoModIgnoresNodeModules(t *testing.T) { + root := projectRoot(t) + + cmd := exec.Command("go", "mod", "edit", "-json") + cmd.Dir = root + out, runErr := cmd.Output() + if runErr != nil { + t.Fatalf("go mod edit -json: %v", runErr) + } + + var mod struct { + Ignore []struct { + Path string + } + } + if parseErr := json.Unmarshal(out, &mod); parseErr != nil { + t.Fatalf("parse go mod edit -json output: %v", parseErr) + } + + for _, ig := range mod.Ignore { + if ig.Path == "node_modules" { + return + } + } + t.Errorf("go.mod is missing `ignore node_modules`; " + + "add it with: go mod edit -ignore=node_modules") +} diff --git a/specs/go-ignore-node-modules.md b/specs/go-ignore-node-modules.md new file mode 100644 index 000000000..da3c673b3 --- /dev/null +++ b/specs/go-ignore-node-modules.md @@ -0,0 +1,65 @@ +# Keep node_modules Out of the Go Module + +The repo holds four npm packages (`editors/vscode`, `ctx-desktop`, +`tools/typecheck/opencode`, `tools/typecheck/pi`) inside the root +Go module. npm dependencies may ship Go sources, and nothing kept +Go tooling from treating them as part of ctx. + +## Problem + +`flatted` (pulled in by eslint through `flat-cache`) ships +`flatted/golang/pkg/flatted/flatted.go`. After `npm install` in +`editors/vscode`: + +- `go list ./...` reports + `github.com/ActiveMemory/ctx/editors/vscode/node_modules/flatted/golang/pkg/flatted` + as a package of this module, so `go build`, `go vet`, and + `go test ./...` compile it. +- `make lint` fails with two `govet` findings inside that file. +- `make test` fails in `TestGolangciLint`, which shells out to + golangci-lint. + +CI never saw it, because the Go jobs don't run `npm install`. Any +contributor who works on the extension and then runs `make lint` +does. + +`./...` skips only directories starting with `.` or `_`, +`testdata`, and nested modules; `node_modules` is none of those. +golangci-lint's `exclusions.paths` only hides reported issues: the +package is still loaded and type-checked. + +## Solution + +1. **`go.mod`: `ignore node_modules`** (Go 1.25+). The bare form, + without a `./` prefix, matches a `node_modules` directory at + any depth (`./node_modules` would match only the root, which + was verified to leave the package in `go list ./...`). This + removes the directory from `./...` for every Go command, + golangci-lint included, since it loads packages through the + go command. +2. **`.golangci.yml`: `node_modules` in `exclusions.paths`**, + beside `vendor`, `dist`, and `site`. With (1) in place this is + belt-and-braces; it states in the linter's own config that + node code is not in scope, and holds if golangci-lint is ever + pointed at an explicit path. +3. **Regression guard:** + `internal/compliance/go_mod_ignore_test.go` reads go.mod via + `go mod edit -json` and fails if the directive is missing. + +The compliance tests' own file walkers (`allGoFiles`, +`allSourceFiles`) already skip `node_modules`; they need no +change. + +## Verification + +With `editors/vscode/node_modules` installed: + +- `go list ./... | grep node_modules` prints nothing. +- `make lint`: 0 issues. `make test`: passes. +- `TestGoModIgnoresNodeModules` passes, and fails after + `go mod edit -dropignore=node_modules`. + +## Non-Goals + +- `tools/ctxctl` is a separate module with no npm package inside + it; its go.mod is unchanged.