diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be07784..ebddc89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,13 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + cache-dependency-path: | + go.sum + cli/go.sum - name: Create a new release uses: goreleaser/goreleaser-action@v7 @@ -60,6 +67,7 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} # Use the tag that triggered this run; otherwise goreleaser may pick # a pre-release tag (e.g. v2.6.0-beta5) pointing at the same commit. GORELEASER_CURRENT_TAG: ${{ github.ref_name }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7da401..3ee37d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,24 +12,34 @@ on: jobs: test: + name: Unit tests (${{ matrix.module }}) runs-on: ubuntu-latest permissions: contents: read + strategy: + fail-fast: false + matrix: + # The library and the CLI are separate Go modules, so `./...` from the + # repo root does not reach the CLI. Each module is tested in its own job. + module: ['.', 'cli'] steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: '1.26' + cache-dependency-path: | + go.sum + cli/go.sum - name: Run go vet - run: go vet ./... + run: go -C ${{ matrix.module }} vet ./... - name: Run tests - run: go test -v ./... + run: go -C ${{ matrix.module }} test -v ./... - name: Run tests with race detector - run: go test -race ./... + run: go -C ${{ matrix.module }} test -race ./... integration: name: Integration tests @@ -42,9 +52,13 @@ jobs: - uses: actions/setup-go@v6 with: go-version: '1.26' + cache-dependency-path: integration/go.sum + + - name: Run go vet + run: go -C integration vet ./... - name: Run integration tests - run: cd integration && go test -v -timeout 10m ./... + run: go -C integration test -race -v -timeout 15m ./... build-docker-image: name: Build Docker image diff --git a/.gitignore b/.gitignore index 6b5dfd0..8dc9d71 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ output-posts/ ClAUDE.local.md .claude/settings.local.json +completions/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0c3bf4e..b78a9d9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,11 +6,21 @@ version: 2 before: hooks: - # You may remove this if you don't use go modules. - go mod tidy + - go -C cli mod tidy + # Shell completions are generated once here and shipped in every archive, + # which is also where the Homebrew cask picks them up from. + - rm -rf completions + - mkdir -p completions + - sh -c 'go -C cli run ./cmd/goblog completion bash > completions/goblog.bash' + - sh -c 'go -C cli run ./cmd/goblog completion zsh > completions/goblog.zsh' + - sh -c 'go -C cli run ./cmd/goblog completion fish > completions/goblog.fish' builds: - id: goblog + # The CLI lives in its own leaf module so that library consumers do not + # inherit its dependencies. `main` is resolved relative to `dir`. + dir: cli main: ./cmd/goblog binary: goblog env: @@ -18,6 +28,7 @@ builds: goos: - linux - darwin + # Windows binaries are built but are not a supported install target. - windows goarch: - amd64 @@ -41,6 +52,36 @@ archives: format_overrides: - goos: windows formats: [zip] + files: + - LICENSE + - README.md + - completions/* + +homebrew_casks: + - name: goblog + ids: [goblog] + binaries: [goblog] + repository: + owner: harrydayexe + name: homebrew-tap + branch: main + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + homepage: https://github.com/harrydayexe/GoBlog + description: Create a blog feed from posts written in Markdown + completions: + bash: completions/goblog.bash + zsh: completions/goblog.zsh + fish: completions/goblog.fish + # Only tag non-prerelease versions into the tap. + skip_upload: auto + hooks: + post: + install: | + if OS.mac? + system_command "/usr/bin/xattr", + args: ["-dr", "com.apple.quarantine", "#{staged_path}/goblog"], + must_succeed: false + end checksum: name_template: "checksums.txt" diff --git a/CLAUDE.md b/CLAUDE.md index 71e6ff3..3444f8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,10 +5,20 @@ All API changes (new exported types, functions, options, flags, or fields) must include corresponding documentation updates: godoc comments on the new symbols, relevant sections in `README.md`, and any package-level `doc.go` entries that reference available options or features. The README.md does not need to be flooded with documentation. Just the relevant information for a user to get started with the 3 methods to consume the library: -1. CLI tool via go install +1. CLI tool via Homebrew (`brew install harrydayexe/tap/goblog`) or a release archive — `go install` is not supported 2. Docker image via docker pull/run 3. The library itself +## Modules + +The repository holds three Go modules: + +- `/go.mod` — `github.com/harrydayexe/GoBlog/v2`, the public library (`pkg/...`). Its dependency graph is a budget: anything added here is inherited by every library consumer. +- `/cli/go.mod` — `github.com/harrydayexe/GoBlog/v2/cli`, the `goblog` binary (`cli/cmd/goblog`, `cli/internal/...`). Never published to the module proxy, so it can take any dependency it needs. Use `replace ... => ../` to reach the library. +- `/integration/go.mod` — black-box tests requiring Docker. + +CLI-only code belongs under `cli/`, never at the repo root. `go test ./...`, `go vet ./...` and friends stop at a nested `go.mod`, so run them per module (the `just` recipes already do). + ## Code Style When two approaches are functionally equivalent with no performance difference, prefer the one that is easier to read and understand. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ec20c2..b7cfc38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,14 +25,28 @@ an acknowledgement within a few days. ## Project Structure +The repository holds three Go modules: + ``` -cmd/goblog/ CLI entry point (main package) -pkg/ Public, importable packages -internal/ Private implementation details +go.mod github.com/harrydayexe/GoBlog/v2 (the public library) + pkg/ Public, importable packages +cli/go.mod github.com/harrydayexe/GoBlog/v2/cli (never published) + cli/cmd/goblog/ CLI entry point (main package) + cli/internal/ Private CLI implementation details +integration/go.mod github.com/harrydayexe/GoBlog/v2/integration docs/example-posts/ Sample Markdown posts for local runs .github/workflows/ CI: tests, license-header check, release ``` +The CLI lives in its own leaf module so that anything it imports stays out of +the library's dependency graph. Both `cli/` and `integration/` use +`replace github.com/harrydayexe/GoBlog/v2 => ../`, so they always build against +the library at the current commit — no tag or release is needed in between. + +Because `go test ./...` stops at a nested `go.mod` boundary, commands run from +the repo root cover the library only. The `just` recipes below iterate over +every module, and CI runs a job per module. + ## Development Setup **Prerequisites** @@ -79,9 +93,9 @@ Or run them directly: cd integration && go test -v -timeout 10m ./... ``` -The unit suite (`just test`) deliberately excludes the integration module — -`go test ./...` stops at the nested `go.mod` boundary — so unit feedback -stays fast in CI even when integration tests are slow. +The unit suite (`just test`) covers the library and CLI modules but +deliberately excludes the integration module, so unit feedback stays fast in +CI even when integration tests are slow. In CI the integration tests run in a dedicated `integration` job so the two stages report separately. diff --git a/Dockerfile b/Dockerfile index 354b34e..897359d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,15 +7,17 @@ FROM golang:1.26-alpine AS builder WORKDIR /build -# Copy go mod files +# Copy go mod files for both modules. The CLI module replaces the library with +# ../, so the root go.mod must be present before `go mod download` can resolve. COPY go.mod go.sum ./ -RUN go mod download +COPY cli/go.mod cli/go.sum ./cli/ +RUN go -C cli mod download # Copy source code COPY . . -# Build the binary -RUN CGO_ENABLED=0 GOOS=linux go build -o goblog ./cmd/goblog +# Build the binary from the CLI module +RUN CGO_ENABLED=0 GOOS=linux go -C cli build -o /build/goblog ./cmd/goblog # Runtime stage FROM alpine:latest diff --git a/README.md b/README.md index 3dd3a4b..f27483e 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,30 @@ GoBlog is a blog generation and serving system for creating static blog feeds fr ## CLI -Install the `goblog` binary: +The `goblog` binary lives in its own Go module (`cli/`) that is not published to +the module proxy, so it is installed from a package manager or a release archive +rather than with `go install`. + +**Homebrew** (macOS and Linux): + +```bash +brew install harrydayexe/tap/goblog +``` + +**Release archive** — grab the archive for your platform from the +[releases page](https://github.com/harrydayexe/GoBlog/releases) and put the +binary on your `PATH`: ```bash -go install github.com/harrydayexe/GoBlog/v2/cmd/goblog@latest +curl -sSL https://github.com/harrydayexe/GoBlog/releases/latest/download/GoBlog_Linux_x86_64.tar.gz | tar -xz goblog +sudo install goblog /usr/local/bin/goblog ``` +Archives are published for Linux and macOS on `x86_64` and `arm64`. Windows +archives are built as well but Windows is not a supported install target. + +There is also a [Docker image](#docker) if you only need to serve a blog. + ```bash # Generate static files goblog generate posts/ output/ @@ -59,9 +77,11 @@ When `--base-url` is set, the server also exposes the generated feeds at `{root- ### Shell completion -`goblog` can generate shell completion scripts at runtime. After installing the -binary, source the appropriate script to enable tab-completion of subcommands and -flags. +The Homebrew cask installs bash, zsh, and fish completions for you. Release +archives ship the same scripts in a `completions/` directory. + +`goblog` can also generate them at runtime — source the appropriate script to +enable tab-completion of subcommands and flags. **Bash** — add to `~/.bashrc`: @@ -76,6 +96,12 @@ autoload -Uz compinit && compinit source <(goblog completion zsh) ``` +**Fish** — write the script to your completions directory: + +```fish +goblog completion fish > ~/.config/fish/completions/goblog.fish +``` + ## Docker The official image is [`harrydayexe/goblog`](https://hub.docker.com/repository/docker/harrydayexe/goblog/general). It runs `goblog serve --health-checks /posts` by default and exposes port `8080`. Health-check endpoints are enabled in the Docker image. File watching is off by default; pass `--watch` to enable it. @@ -127,6 +153,9 @@ Add GoBlog as a dependency: go get github.com/harrydayexe/GoBlog/v2 ``` +The CLI is a separate module (`cli/`) that is never published, so none of its +dependencies reach your build. + The main packages are: | Package | Summary | @@ -193,7 +222,7 @@ cfg := config.ServerConfig{ config.WithLogger(logger).AsServerOption(), }, } -srv, err := server.New(nil, postsFS, cfg) +srv, err := server.New(postsFS, cfg) // Watcher w, err := watcher.New("posts/", config.WithLogger(logger).AsWatcherOption()) diff --git a/cmd/goblog/main.go b/cli/cmd/goblog/main.go similarity index 85% rename from cmd/goblog/main.go rename to cli/cmd/goblog/main.go index fccd3e9..8149a1b 100644 --- a/cmd/goblog/main.go +++ b/cli/cmd/goblog/main.go @@ -10,11 +10,11 @@ import ( "log/slog" "os" - "github.com/harrydayexe/GoBlog/v2/internal/cliflags" - "github.com/harrydayexe/GoBlog/v2/internal/generator" - loggermod "github.com/harrydayexe/GoBlog/v2/internal/logger" - "github.com/harrydayexe/GoBlog/v2/internal/server" - "github.com/harrydayexe/GoBlog/v2/internal/utilities" + "github.com/harrydayexe/GoBlog/v2/cli/internal/cliflags" + "github.com/harrydayexe/GoBlog/v2/cli/internal/generator" + loggermod "github.com/harrydayexe/GoBlog/v2/cli/internal/logger" + "github.com/harrydayexe/GoBlog/v2/cli/internal/server" + "github.com/harrydayexe/GoBlog/v2/cli/internal/utilities" "github.com/urfave/cli/v3" ) diff --git a/cmd/goblog/version.go b/cli/cmd/goblog/version.go similarity index 57% rename from cmd/goblog/version.go rename to cli/cmd/goblog/version.go index 5a98764..50e676c 100644 --- a/cmd/goblog/version.go +++ b/cli/cmd/goblog/version.go @@ -7,10 +7,12 @@ package main import "runtime/debug" // buildVersion returns the version string for the running binary. -// When GoReleaser ldflags are present the injected value is returned as-is. -// Otherwise the value is derived from the embedded Go module build metadata, -// so binaries installed via go install report the module tag. It is safe to -// call from multiple goroutines. +// Released binaries carry the version injected by GoReleaser's ldflags, which +// is returned as-is. The CLI module is never published to the module proxy, so +// a locally built binary has no module tag to fall back on; the embedded Go +// module build metadata is still consulted in case one is present, and +// otherwise the version reads "dev". It is safe to call from multiple +// goroutines. func buildVersion() string { if version != "dev" { return version diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..3389d3a --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,31 @@ +module github.com/harrydayexe/GoBlog/v2/cli + +go 1.26.3 + +// The CLI is never published as a Go module; it is always built from source in +// this repository, so it tracks the library at the same commit. +replace github.com/harrydayexe/GoBlog/v2 => ../ + +require ( + github.com/fatih/color v1.18.0 + github.com/harrydayexe/GoBlog/v2 v2.0.0 + github.com/harrydayexe/GoWebUtilities v1.5.1 + github.com/urfave/cli/v3 v3.6.1 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/alecthomas/chroma/v2 v2.22.0 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/gorilla/feeds v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/yuin/goldmark v1.7.16 // indirect + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect + go.abhg.dev/goldmark/frontmatter v0.3.0 // indirect + go.abhg.dev/goldmark/wikilink v0.6.0 // indirect + golang.org/x/sys v0.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000..4e544eb --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,66 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/chroma/v2 v2.22.0 h1:PqEhf+ezz5F5owoDeOUKFzW+W3ZJDShNCaHg4sZuItI= +github.com/alecthomas/chroma/v2 v2.22.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= +github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= +github.com/harrydayexe/GoWebUtilities v1.5.1 h1:1E/5IEBHdG8SfIwF2tqJhxypsk9VH1TIb1ofMmI6ZOg= +github.com/harrydayexe/GoWebUtilities v1.5.1/go.mod h1:msGwhqkUbSAhhIR+uFnLE08OhBCwuJW0BQ5kUnBvI6I= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= +github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= +go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA= +go.abhg.dev/goldmark/frontmatter v0.3.0/go.mod h1:W3KXvVveKKxU1FIFZ7fgFFQrlkcolnDcOVmu19cCO9U= +go.abhg.dev/goldmark/wikilink v0.6.0 h1:SKZANgMD7GMbaU0kBKTh52Ea9k3A3Y5ZifHoEPC1fuo= +go.abhg.dev/goldmark/wikilink v0.6.0/go.mod h1:Sfaovp00aAVJ5khqIeDTTgkIfZrcurmJGlbntCJUbJY= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cliflags/flags.go b/cli/internal/cliflags/flags.go similarity index 100% rename from internal/cliflags/flags.go rename to cli/internal/cliflags/flags.go diff --git a/internal/errors/doc.go b/cli/internal/errors/doc.go similarity index 100% rename from internal/errors/doc.go rename to cli/internal/errors/doc.go diff --git a/internal/errors/errorType.go b/cli/internal/errors/errorType.go similarity index 100% rename from internal/errors/errorType.go rename to cli/internal/errors/errorType.go diff --git a/internal/errors/errors_test.go b/cli/internal/errors/errors_test.go similarity index 100% rename from internal/errors/errors_test.go rename to cli/internal/errors/errors_test.go diff --git a/internal/errors/inputDirectoryErrors.go b/cli/internal/errors/inputDirectoryErrors.go similarity index 100% rename from internal/errors/inputDirectoryErrors.go rename to cli/internal/errors/inputDirectoryErrors.go diff --git a/internal/generator/command.go b/cli/internal/generator/command.go similarity index 100% rename from internal/generator/command.go rename to cli/internal/generator/command.go diff --git a/internal/generator/flagConsts.go b/cli/internal/generator/flagConsts.go similarity index 100% rename from internal/generator/flagConsts.go rename to cli/internal/generator/flagConsts.go diff --git a/internal/generator/generator.go b/cli/internal/generator/generator.go similarity index 96% rename from internal/generator/generator.go rename to cli/internal/generator/generator.go index c4a0549..9a88b66 100644 --- a/internal/generator/generator.go +++ b/cli/internal/generator/generator.go @@ -12,8 +12,8 @@ import ( "path/filepath" "strings" - "github.com/harrydayexe/GoBlog/v2/internal/cliflags" - "github.com/harrydayexe/GoBlog/v2/internal/utilities" + "github.com/harrydayexe/GoBlog/v2/cli/internal/cliflags" + "github.com/harrydayexe/GoBlog/v2/cli/internal/utilities" "github.com/harrydayexe/GoBlog/v2/pkg/config" "github.com/harrydayexe/GoBlog/v2/pkg/generator" "github.com/harrydayexe/GoBlog/v2/pkg/outputter" diff --git a/internal/generator/generator_test.go b/cli/internal/generator/generator_test.go similarity index 94% rename from internal/generator/generator_test.go rename to cli/internal/generator/generator_test.go index dfe9653..42b4d71 100644 --- a/internal/generator/generator_test.go +++ b/cli/internal/generator/generator_test.go @@ -13,6 +13,7 @@ import ( "github.com/harrydayexe/GoBlog/v2/pkg/config" "github.com/harrydayexe/GoBlog/v2/pkg/generator" "github.com/harrydayexe/GoBlog/v2/pkg/outputter" + "github.com/harrydayexe/GoBlog/v2/pkg/templates" ) // TestRunGenerate tests the core generate logic. @@ -53,7 +54,7 @@ This is a test post. // Create filesystem and renderer postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -114,7 +115,7 @@ tags: [test] } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -155,7 +156,7 @@ func TestRunGenerate_EmptyPosts(t *testing.T) { } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -203,7 +204,7 @@ No description or date. } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -252,7 +253,7 @@ description: A test } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -308,7 +309,7 @@ Testing the blog root feature. } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } @@ -408,7 +409,7 @@ tags: [test, go] } postsFsys := os.DirFS(postsDir) - renderer, err := generator.NewTemplateRenderer(os.DirFS("../../pkg/templates/default")) + renderer, err := generator.NewTemplateRenderer(templates.Default) if err != nil { t.Fatalf("Failed to create renderer: %v", err) } diff --git a/internal/logger/cli.go b/cli/internal/logger/cli.go similarity index 100% rename from internal/logger/cli.go rename to cli/internal/logger/cli.go diff --git a/internal/logger/cli_test.go b/cli/internal/logger/cli_test.go similarity index 100% rename from internal/logger/cli_test.go rename to cli/internal/logger/cli_test.go diff --git a/internal/server/command.go b/cli/internal/server/command.go similarity index 100% rename from internal/server/command.go rename to cli/internal/server/command.go diff --git a/internal/server/command_test.go b/cli/internal/server/command_test.go similarity index 93% rename from internal/server/command_test.go rename to cli/internal/server/command_test.go index 3e25ba3..6f897b7 100644 --- a/internal/server/command_test.go +++ b/cli/internal/server/command_test.go @@ -6,8 +6,6 @@ package server import ( "context" - "io" - "log/slog" "net/http" "net/http/httptest" "os" @@ -41,10 +39,6 @@ func testFS() fstest.MapFS { } } -func discardLogger() *slog.Logger { - return slog.New(slog.NewTextHandler(io.Discard, nil)) -} - // TestRunServe_CanceledContext verifies that runServe returns nil when the context is canceled. func TestRunServe_CanceledContext(t *testing.T) { t.Parallel() @@ -70,7 +64,7 @@ func TestRunServe_ServesIndex(t *testing.T) { Server: []config.BaseServerOption{config.WithPort(0)}, } - srv, err := pkgserver.New(discardLogger(), testFS(), cfg) + srv, err := pkgserver.New(testFS(), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -92,7 +86,7 @@ func TestRunServe_ServesPost(t *testing.T) { Server: []config.BaseServerOption{config.WithPort(0)}, } - srv, err := pkgserver.New(discardLogger(), testFS(), cfg) + srv, err := pkgserver.New(testFS(), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -117,7 +111,7 @@ func TestRunServe_BlogRoot(t *testing.T) { }, } - srv, err := pkgserver.New(discardLogger(), testFS(), cfg) + srv, err := pkgserver.New(testFS(), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -173,7 +167,7 @@ func TestRunServe_WatchReloadsPost(t *testing.T) { Server: []config.BaseServerOption{config.WithPort(0)}, } - srv, err := pkgserver.New(discardLogger(), os.DirFS(dir), cfg) + srv, err := pkgserver.New(os.DirFS(dir), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -234,7 +228,7 @@ func TestRunServe_ServesFeedsWithBaseURL(t *testing.T) { }, } - srv, err := pkgserver.New(discardLogger(), testFS(), cfg) + srv, err := pkgserver.New(testFS(), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -260,7 +254,7 @@ func TestRunServe_FeedsNotAvailableWithoutBaseURL(t *testing.T) { // No WithBaseURL → generator skips feed generation. } - srv, err := pkgserver.New(discardLogger(), testFS(), cfg) + srv, err := pkgserver.New(testFS(), cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } diff --git a/internal/server/flagConsts.go b/cli/internal/server/flagConsts.go similarity index 100% rename from internal/server/flagConsts.go rename to cli/internal/server/flagConsts.go diff --git a/internal/server/server.go b/cli/internal/server/server.go similarity index 96% rename from internal/server/server.go rename to cli/internal/server/server.go index 07f0748..203f0d4 100644 --- a/internal/server/server.go +++ b/cli/internal/server/server.go @@ -12,8 +12,8 @@ import ( "path" "strings" - "github.com/harrydayexe/GoBlog/v2/internal/cliflags" - "github.com/harrydayexe/GoBlog/v2/internal/utilities" + "github.com/harrydayexe/GoBlog/v2/cli/internal/cliflags" + "github.com/harrydayexe/GoBlog/v2/cli/internal/utilities" "github.com/harrydayexe/GoBlog/v2/pkg/config" "github.com/harrydayexe/GoBlog/v2/pkg/server" "github.com/harrydayexe/GoBlog/v2/pkg/templates" @@ -116,7 +116,7 @@ func NewServeCommand(ctx context.Context, c *cli.Command) error { } func runServe(ctx context.Context, postsPath string, posts fs.FS, cfg config.ServerConfig, watch bool) error { - srv, err := server.New(nil, posts, cfg) + srv, err := server.New(posts, cfg) if err != nil { return err } diff --git a/internal/utilities/cliErrorHandler.go b/cli/internal/utilities/cliErrorHandler.go similarity index 92% rename from internal/utilities/cliErrorHandler.go rename to cli/internal/utilities/cliErrorHandler.go index 4e93eb4..c43febe 100644 --- a/internal/utilities/cliErrorHandler.go +++ b/cli/internal/utilities/cliErrorHandler.go @@ -10,7 +10,7 @@ import ( "os" "github.com/fatih/color" - inerrors "github.com/harrydayexe/GoBlog/v2/internal/errors" + inerrors "github.com/harrydayexe/GoBlog/v2/cli/internal/errors" ) // CliErrorHandler handles errors by printing them to stdout or stderr and exits if fatal. diff --git a/internal/utilities/doc.go b/cli/internal/utilities/doc.go similarity index 100% rename from internal/utilities/doc.go rename to cli/internal/utilities/doc.go diff --git a/internal/utilities/getDirectoryFromInput.go b/cli/internal/utilities/getDirectoryFromInput.go similarity index 93% rename from internal/utilities/getDirectoryFromInput.go rename to cli/internal/utilities/getDirectoryFromInput.go index 2df6505..d477645 100644 --- a/internal/utilities/getDirectoryFromInput.go +++ b/cli/internal/utilities/getDirectoryFromInput.go @@ -8,7 +8,7 @@ import ( "os" "path/filepath" - "github.com/harrydayexe/GoBlog/v2/internal/errors" + "github.com/harrydayexe/GoBlog/v2/cli/internal/errors" ) // GetDirectoryFromInput validates a path and returns it as an fs.FS. diff --git a/internal/utilities/openAssetsDir.go b/cli/internal/utilities/openAssetsDir.go similarity index 100% rename from internal/utilities/openAssetsDir.go rename to cli/internal/utilities/openAssetsDir.go diff --git a/internal/utilities/utilities_test.go b/cli/internal/utilities/utilities_test.go similarity index 99% rename from internal/utilities/utilities_test.go rename to cli/internal/utilities/utilities_test.go index 388e0c4..bf272c7 100644 --- a/internal/utilities/utilities_test.go +++ b/cli/internal/utilities/utilities_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - inerrors "github.com/harrydayexe/GoBlog/v2/internal/errors" + inerrors "github.com/harrydayexe/GoBlog/v2/cli/internal/errors" ) // TestGetDirectoryFromInput_ValidAbsolutePath tests with a valid absolute path. diff --git a/go.mod b/go.mod index da7333c..90dfa54 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,9 @@ go 1.26.3 require ( github.com/alecthomas/chroma/v2 v2.22.0 - github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.10.1 github.com/gorilla/feeds v1.2.0 github.com/harrydayexe/GoWebUtilities v1.5.1 - github.com/urfave/cli/v3 v3.6.1 github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.abhg.dev/goldmark/frontmatter v0.3.0 @@ -19,8 +17,6 @@ require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/caarlos0/env/v11 v11.3.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4e544eb..4ec2f55 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= @@ -31,11 +29,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -44,8 +37,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= -github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= @@ -55,8 +46,6 @@ go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZf go.abhg.dev/goldmark/frontmatter v0.3.0/go.mod h1:W3KXvVveKKxU1FIFZ7fgFFQrlkcolnDcOVmu19cCO9U= go.abhg.dev/goldmark/wikilink v0.6.0 h1:SKZANgMD7GMbaU0kBKTh52Ea9k3A3Y5ZifHoEPC1fuo= go.abhg.dev/goldmark/wikilink v0.6.0/go.mod h1:Sfaovp00aAVJ5khqIeDTTgkIfZrcurmJGlbntCJUbJY= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/integration/lifecycle_test.go b/integration/lifecycle_test.go index 89978fe..0a0bec5 100644 --- a/integration/lifecycle_test.go +++ b/integration/lifecycle_test.go @@ -38,7 +38,7 @@ func TestRun_BindError(t *testing.T) { config.WithHost("127.0.0.1"), }, } - srv, err := server.New(nil, os.DirFS(dir), cfg) + srv, err := server.New(os.DirFS(dir), cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -77,7 +77,7 @@ func TestRun_GracefulShutdown(t *testing.T) { config.WithHost("127.0.0.1"), }, } - srv, err := server.New(nil, os.DirFS(dir), cfg) + srv, err := server.New(os.DirFS(dir), cfg) if err != nil { t.Fatalf("server.New: %v", err) } diff --git a/justfile b/justfile index 2087fa5..168ce81 100644 --- a/justfile +++ b/justfile @@ -4,10 +4,16 @@ # Variables BINARY_NAME := "goblog" MODULE := "github.com/harrydayexe/GoBlog/v2" +CLI_DIR := "cli" MAIN_PATH := "./cmd/goblog" DIST_DIR := "dist" COVERAGE_DIR := "coverage" +# Modules covered by the unit suite. The integration module is excluded because +# it needs Docker and is run separately by `test-integration`. +UNIT_MODULES := ". cli" +ALL_MODULES := ". cli integration" + # Build-time version injection using git tags VERSION := `git describe --tags --always --dirty 2>/dev/null || echo "dev"` LDFLAGS := '-s -w -X main.version=' + VERSION @@ -18,13 +24,13 @@ LDFLAGS := '-s -w -X main.version=' + VERSION build: @echo "Building {{BINARY_NAME}}..." @mkdir -p {{DIST_DIR}} - go build -ldflags "{{LDFLAGS}}" -o {{DIST_DIR}}/{{BINARY_NAME}} {{MAIN_PATH}} + go -C {{CLI_DIR}} build -ldflags "{{LDFLAGS}}" -o {{justfile_directory()}}/{{DIST_DIR}}/{{BINARY_NAME}} {{MAIN_PATH}} @echo "✓ Binary built successfully: {{DIST_DIR}}/{{BINARY_NAME}}" # Build and install to $GOPATH/bin install: @echo "Installing {{BINARY_NAME}}..." - go install -ldflags "{{LDFLAGS}}" {{MAIN_PATH}} + go -C {{CLI_DIR}} install -ldflags "{{LDFLAGS}}" {{MAIN_PATH}} @echo "✓ Binary installed successfully" # Remove build artifacts @@ -39,40 +45,61 @@ clean: # Run all tests [group("test")] test: - go test ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{UNIT_MODULES}}; do + echo "==> $mod" + go -C "$mod" test ./... + done # Run tests with verbose output [group("test")] test-verbose: - go test -v ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{UNIT_MODULES}}; do + echo "==> $mod" + go -C "$mod" test -v ./... + done # Run tests with race detector [group("test")] test-race: - go test -race ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{UNIT_MODULES}}; do + echo "==> $mod" + go -C "$mod" test -race ./... + done # Run integration tests (requires Docker) [group("test")] test-integration: - cd integration && go test -v -timeout 10m ./... + go -C integration test -v -timeout 10m ./... # Run tests with coverage profile [group("test")] test-coverage: + # The two modules produce separate profiles: `go tool cover` resolves source + # files through the module it runs in, so a merged cross-module profile + # would not be readable. @echo "Running tests with coverage..." @mkdir -p {{COVERAGE_DIR}} - go test -coverprofile={{COVERAGE_DIR}}/coverage.out ./... + go test -coverprofile={{COVERAGE_DIR}}/library.out ./... + go -C {{CLI_DIR}} test -coverprofile={{justfile_directory()}}/{{COVERAGE_DIR}}/cli.out ./... @echo "\nCoverage summary:" - @go tool cover -func={{COVERAGE_DIR}}/coverage.out | tail -1 + @printf 'library ' && go tool cover -func={{COVERAGE_DIR}}/library.out | tail -1 + @printf 'cli ' && go -C {{CLI_DIR}} tool cover -func={{justfile_directory()}}/{{COVERAGE_DIR}}/cli.out | tail -1 -# Generate HTML coverage report +# Generate HTML coverage reports [group("test")] coverage-html: test-coverage - @echo "Generating HTML coverage report..." - go tool cover -html={{COVERAGE_DIR}}/coverage.out -o {{COVERAGE_DIR}}/coverage.html - @echo "✓ Coverage report: {{COVERAGE_DIR}}/coverage.html" + @echo "Generating HTML coverage reports..." + go tool cover -html={{COVERAGE_DIR}}/library.out -o {{COVERAGE_DIR}}/library.html + go -C {{CLI_DIR}} tool cover -html={{justfile_directory()}}/{{COVERAGE_DIR}}/cli.out -o {{justfile_directory()}}/{{COVERAGE_DIR}}/cli.html + @echo "✓ Coverage reports: {{COVERAGE_DIR}}/library.html, {{COVERAGE_DIR}}/cli.html" @echo "Opening in browser..." - @open {{COVERAGE_DIR}}/coverage.html 2>/dev/null || xdg-open {{COVERAGE_DIR}}/coverage.html 2>/dev/null || echo "Please open {{COVERAGE_DIR}}/coverage.html manually" + @open {{COVERAGE_DIR}}/library.html 2>/dev/null || xdg-open {{COVERAGE_DIR}}/library.html 2>/dev/null || echo "Please open {{COVERAGE_DIR}}/library.html manually" # Run complete test suite (CI/CD simulation) [group("test")] @@ -89,23 +116,40 @@ test-all: # Run go vet linter [group("lint")] vet: - go vet ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{ALL_MODULES}}; do + echo "==> $mod" + go -C "$mod" vet ./... + done # Format all Go code [group("lint")] fmt: - go fmt ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{ALL_MODULES}}; do + go -C "$mod" fmt ./... + done # Run vulncheck on codebase [group("lint")] vulncheck: - govulncheck ./... + #!/usr/bin/env bash + set -euo pipefail + for mod in {{ALL_MODULES}}; do + echo "==> $mod" + (cd "$mod" && govulncheck ./...) + done # Run go mod tidy [group("lint")] mod-tidy: - go mod tidy - cd integration && go mod tidy + #!/usr/bin/env bash + set -euo pipefail + for mod in {{ALL_MODULES}}; do + go -C "$mod" mod tidy + done # Check if code is formatted [group("lint")] @@ -128,19 +172,19 @@ check-license: add-license: addlicense -l mpl -c "GoBlog Authors" ./ -# Run generator command with arguments +# Run generator command with arguments. Paths are relative to the repo root. [group('run')] -run-gen *ARGS: - go run {{MAIN_PATH}} gen {{ARGS}} +run-gen *ARGS: build + {{DIST_DIR}}/{{BINARY_NAME}} generate {{ARGS}} # Run serve command with optional arguments (defaults to example posts) [group('run')] -run-serve *ARGS: +run-serve *ARGS: build #!/usr/bin/env bash if [ -z "{{ARGS}}" ]; then - go run {{MAIN_PATH}} serve docs/example-posts + {{DIST_DIR}}/{{BINARY_NAME}} serve docs/example-posts else - go run {{MAIN_PATH}} serve {{ARGS}} + {{DIST_DIR}}/{{BINARY_NAME}} serve {{ARGS}} fi # Build the dockerfile for the current architecture diff --git a/pkg/server/assets_test.go b/pkg/server/assets_test.go index acddb99..e823f05 100644 --- a/pkg/server/assets_test.go +++ b/pkg/server/assets_test.go @@ -6,7 +6,6 @@ package server_test import ( "io" - "log/slog" "net/http" "net/http/httptest" "os" @@ -37,7 +36,7 @@ func newAssetsServer(t *testing.T, opts ...config.BaseServerOption) *server.Serv Server: opts, Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(slog.New(slog.DiscardHandler), createTestFS(t), cfg) + srv, err := server.New(createTestFS(t), cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -135,7 +134,7 @@ func TestAssets_Traversal(t *testing.T) { } t.Cleanup(func() { root.Close() }) - h := server.Handler(&generator.GeneratedBlog{}, nil, config.WithAssetsDir(root.FS())) + h := server.Handler(&generator.GeneratedBlog{}, config.WithAssetsDir(root.FS())) for _, target := range []string{ "/images/../secret.txt", @@ -165,7 +164,7 @@ func TestAssets_NoRouteWhenDisabled(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - h := server.Handler(&generator.GeneratedBlog{}, nil, tt.opts...) + h := server.Handler(&generator.GeneratedBlog{}, tt.opts...) if w := get(h, "/images/pipeline.png"); w.Code != http.StatusNotFound { t.Errorf("status %d, want 404", w.Code) } diff --git a/pkg/server/cache_control_test.go b/pkg/server/cache_control_test.go index fe70e37..428788c 100644 --- a/pkg/server/cache_control_test.go +++ b/pkg/server/cache_control_test.go @@ -6,10 +6,8 @@ package server_test import ( "context" - "log/slog" "net/http" "net/http/httptest" - "os" "testing" "time" @@ -22,14 +20,13 @@ import ( func TestCacheControl_Default(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -54,7 +51,6 @@ func TestCacheControl_Default(t *testing.T) { func TestCacheControl_CustomTTL(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -64,7 +60,7 @@ func TestCacheControl_CustomTTL(t *testing.T) { Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -85,7 +81,6 @@ func TestCacheControl_CustomTTL(t *testing.T) { func TestCacheControl_ZeroDisablesHeader(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -95,7 +90,7 @@ func TestCacheControl_ZeroDisablesHeader(t *testing.T) { Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -114,7 +109,6 @@ func TestCacheControl_ZeroDisablesHeader(t *testing.T) { func TestCacheControl_PersistsAcrossUpdates(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -124,7 +118,7 @@ func TestCacheControl_PersistsAcrossUpdates(t *testing.T) { Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } diff --git a/pkg/server/doc.go b/pkg/server/doc.go index 60de358..e027909 100644 --- a/pkg/server/doc.go +++ b/pkg/server/doc.go @@ -32,7 +32,7 @@ // }, // } // -// srv, err := server.New(nil, postsFS, cfg) +// srv, err := server.New(postsFS, cfg) // if err != nil { // log.Fatal(err) // } @@ -113,7 +113,7 @@ // }, // } // -// srv, err := server.New(nil, postsFS, cfg) +// srv, err := server.New(postsFS, cfg) // // Custom middleware can be added following the standard pattern: // @@ -180,7 +180,7 @@ // config.WithHealthChecks(), // }, // } -// srv, err := server.New(nil, postsFS, cfg) +// srv, err := server.New(postsFS, cfg) // // Three unauthenticated GET endpoints are exposed: // diff --git a/pkg/server/feed_test.go b/pkg/server/feed_test.go index 58819cc..3d7ed82 100644 --- a/pkg/server/feed_test.go +++ b/pkg/server/feed_test.go @@ -37,7 +37,7 @@ func TestHandler_SiteRSSFeed(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/rss.xml", nil) @@ -58,7 +58,7 @@ func TestHandler_SiteAtomFeed(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/atom.xml", nil) @@ -79,7 +79,7 @@ func TestHandler_SiteRSSFeed_Empty(t *testing.T) { t.Parallel() blog := generator.NewEmptyGeneratedBlog() // no feed bytes - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/rss.xml", nil) @@ -96,7 +96,7 @@ func TestHandler_SiteAtomFeed_Empty(t *testing.T) { t.Parallel() blog := generator.NewEmptyGeneratedBlog() // no feed bytes - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/atom.xml", nil) @@ -113,7 +113,7 @@ func TestHandler_TagRSSFeed(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("golang") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/tags/golang.rss.xml", nil) @@ -134,7 +134,7 @@ func TestHandler_TagAtomFeed(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("golang") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/tags/golang.atom.xml", nil) @@ -154,7 +154,7 @@ func TestHandler_TagRSSFeed_UnknownTag(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("golang") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/tags/rust.rss.xml", nil) @@ -170,7 +170,7 @@ func TestHandler_TagAtomFeed_UnknownTag(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("golang") - h := server.Handler(blog, nil) + h := server.Handler(blog) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/tags/rust.atom.xml", nil) @@ -187,7 +187,7 @@ func TestHandler_FeedsWithBlogRoot(t *testing.T) { t.Parallel() blog := testBlogWithFeeds("golang") - h := server.Handler(blog, nil, config.WithBlogRoot("/blog/")) + h := server.Handler(blog, config.WithBlogRoot("/blog/")) tests := []struct { path string diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 25d3b1e..98e41ac 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -66,12 +66,8 @@ type HandlerConfig struct { // // Supply a logger via [config.WithLogger] in opts: // -// h := server.Handler(blog, nil, config.WithLogger(myLogger), config.WithBlogRoot("/blog/")) -// -// Deprecated: the positional logger parameter will be removed in v3.0.0. -// Pass nil and supply the logger via config.WithLogger in opts instead. -// When both are provided, the config.WithLogger option takes precedence. -func Handler(blog *generator.GeneratedBlog, logger *slog.Logger, opts ...config.BaseOption) http.Handler { +// h := server.Handler(blog, config.WithLogger(myLogger), config.WithBlogRoot("/blog/")) +func Handler(blog *generator.GeneratedBlog, opts ...config.BaseOption) http.Handler { cfg := HandlerConfig{ BlogRoot: config.BlogRoot("/"), } @@ -86,13 +82,8 @@ func Handler(blog *generator.GeneratedBlog, logger *slog.Logger, opts ...config. } } - // Precedence: WithLogger option > positional logger arg > slog.Default(). if cfg.Logger.Logger == nil { - if logger != nil { - cfg.Logger.Logger = logger - } else { - cfg.Logger.Logger = slog.Default() - } + cfg.Logger.Logger = slog.Default() } trimmed := strings.Trim(string(cfg.BlogRoot), "/") diff --git a/pkg/server/health_test.go b/pkg/server/health_test.go index a58eda7..7688c9d 100644 --- a/pkg/server/health_test.go +++ b/pkg/server/health_test.go @@ -31,7 +31,7 @@ func TestHealthChecks_Disabled(t *testing.T) { cfg := config.ServerConfig{ Server: []config.BaseServerOption{config.WithPort(8080)}, } - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -60,7 +60,7 @@ func TestHealthChecks_Live_AlwaysOK(t *testing.T) { }, } // New returns immediately with state=starting when health checks are on. - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -90,7 +90,7 @@ func TestHealthChecks_Ready_WhileStarting(t *testing.T) { }, } // State is "starting" immediately after New (before Run). - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -122,7 +122,7 @@ func TestHealthChecks_NonGetMethod(t *testing.T) { config.WithHealthChecks(), }, } - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -166,7 +166,7 @@ func TestHealthChecks_BypassesMiddleware(t *testing.T) { // With health checks disabled the sync path is used (middleware is applied // but health routes are not intercepted). Enable health checks to get the // pre-middleware interception. - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -203,7 +203,7 @@ func TestHealthChecks_ReadyAfterInit(t *testing.T) { config.WithHealthChecks(), }, } - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("server.New: %v", err) } @@ -266,7 +266,7 @@ func TestHealthChecks_FailedInit(t *testing.T) { config.WithHealthChecks(), }, } - srv, err := server.New(nil, brokenFS, cfg) + srv, err := server.New(brokenFS, cfg) if err != nil { t.Fatalf("server.New: %v (want nil when health checks enabled)", err) } diff --git a/pkg/server/logger_test.go b/pkg/server/logger_test.go index 857f070..80c2646 100644 --- a/pkg/server/logger_test.go +++ b/pkg/server/logger_test.go @@ -47,7 +47,7 @@ func TestServer_LoggerPropagatedToGenerator(t *testing.T) { Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := New(nil, postsFS, cfg) + srv, err := New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } diff --git a/pkg/server/middleware_test.go b/pkg/server/middleware_test.go index a6f27aa..c4a4ef9 100644 --- a/pkg/server/middleware_test.go +++ b/pkg/server/middleware_test.go @@ -5,9 +5,7 @@ package server_test import ( - "bytes" "context" - "io" "io/fs" "log/slog" "net/http" @@ -24,7 +22,6 @@ import ( // TestServerWithoutMiddleware verifies that servers without middleware work correctly // (backward compatibility). func TestServerWithoutMiddleware(t *testing.T) { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -36,7 +33,7 @@ func TestServerWithoutMiddleware(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -53,7 +50,6 @@ func TestServerWithoutMiddleware(t *testing.T) { // TestServerWithSingleMiddleware tests that a single middleware is correctly applied. func TestServerWithSingleMiddleware(t *testing.T) { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) // Track if middleware was called @@ -76,7 +72,7 @@ func TestServerWithSingleMiddleware(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -97,7 +93,6 @@ func TestServerWithSingleMiddleware(t *testing.T) { // TestServerWithMultipleMiddleware tests that multiple middleware are chained correctly. func TestServerWithMultipleMiddleware(t *testing.T) { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) // Track middleware execution @@ -131,7 +126,7 @@ func TestServerWithMultipleMiddleware(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -172,7 +167,6 @@ func TestServerWithMultipleMiddleware(t *testing.T) { // TestMiddlewarePersistsAcrossUpdates verifies that middleware continues to work // after UpdatePosts() is called. func TestMiddlewarePersistsAcrossUpdates(t *testing.T) { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) var callCount int @@ -193,7 +187,7 @@ func TestMiddlewarePersistsAcrossUpdates(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -226,7 +220,6 @@ func TestMiddlewarePersistsAcrossUpdates(t *testing.T) { // TestMultipleWithMiddlewareCalls tests that multiple WithMiddleware calls // correctly append to the middleware chain. func TestMultipleWithMiddlewareCalls(t *testing.T) { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) firstMiddleware := func(h http.Handler) http.Handler { @@ -254,7 +247,7 @@ func TestMultipleWithMiddlewareCalls(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -297,7 +290,7 @@ func ExampleServer_withMiddleware() { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { logger.Error("failed to create server", "error", err) return @@ -312,7 +305,6 @@ func ExampleServer_withMiddleware() { func TestServerDisableTags(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -324,7 +316,7 @@ func TestServerDisableTags(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -354,7 +346,6 @@ func TestServerDisableTags(t *testing.T) { func TestServerTagsEnabledByDefault(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -363,7 +354,7 @@ func TestServerTagsEnabledByDefault(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -383,7 +374,6 @@ func TestServerTagsEnabledByDefault(t *testing.T) { func TestServer_StripsHTMLExtension(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -392,7 +382,7 @@ func TestServer_StripsHTMLExtension(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -448,7 +438,6 @@ func TestServer_StripsHTMLExtension(t *testing.T) { func TestServer_StripsHTMLExtension_BlogRoot(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) cfg := config.ServerConfig{ @@ -461,7 +450,7 @@ func TestServer_StripsHTMLExtension_BlogRoot(t *testing.T) { }, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -489,14 +478,13 @@ func TestServer_StripsHTMLExtension_BlogRoot(t *testing.T) { func TestHandler_StripsHTMLExtension(t *testing.T) { t.Parallel() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) postsFS := createTestFS(t) // Build a GeneratedBlog manually via the generator. cfg := config.ServerConfig{ Gen: []config.GeneratorOption{config.WithRawOutput()}, } - srv, err := server.New(logger, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("failed to create server: %v", err) } @@ -510,56 +498,6 @@ func TestHandler_StripsHTMLExtension(t *testing.T) { } } -// TestServer_WithLoggerOptionTakesPrecedence verifies that config.WithLogger in -// cfg.Server takes precedence over the deprecated positional logger argument. -func TestServer_WithLoggerOptionTakesPrecedence(t *testing.T) { - t.Parallel() - - var optionBuf bytes.Buffer - optionLogger := slog.New(slog.NewTextHandler(&optionBuf, &slog.HandlerOptions{Level: slog.LevelDebug})) - - positionalLogger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - postsFS := createTestFS(t) - cfg := config.ServerConfig{ - Server: []config.BaseServerOption{ - config.WithLogger(optionLogger).AsServerOption(), - }, - Gen: []config.GeneratorOption{config.WithRawOutput()}, - } - - srv, err := server.New(positionalLogger, postsFS, cfg) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - - if srv.Logger.Logger != optionLogger { - t.Error("expected WithLogger option to take precedence over positional logger arg") - } -} - -// TestServer_PositionalLoggerFallback verifies that the positional logger is -// used when no WithLogger option is provided (deprecated path still works). -func TestServer_PositionalLoggerFallback(t *testing.T) { - t.Parallel() - - positionalLogger := slog.New(slog.NewTextHandler(io.Discard, nil)) - - postsFS := createTestFS(t) - cfg := config.ServerConfig{ - Gen: []config.GeneratorOption{config.WithRawOutput()}, - } - - srv, err := server.New(positionalLogger, postsFS, cfg) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - - if srv.Logger.Logger != positionalLogger { - t.Error("expected positional logger to be used when no WithLogger option is provided") - } -} - // createTestFS creates a minimal test filesystem with a single post. func createTestFS(t *testing.T) fs.FS { t.Helper() diff --git a/pkg/server/server.go b/pkg/server/server.go index e3cd4a2..537debd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -106,11 +106,7 @@ type Server struct { // Supply a logger via [config.WithLogger] in cfg.Server: // // cfg.Server = append(cfg.Server, config.WithLogger(myLogger).AsServerOption()) -// -// Deprecated: the positional logger parameter will be removed in v3.0.0. -// Pass nil and supply the logger via config.WithLogger in cfg.Server instead. -// When both are provided, the config.WithLogger option takes precedence. -func New(logger *slog.Logger, posts fs.FS, opts config.ServerConfig) (*Server, error) { +func New(posts fs.FS, opts config.ServerConfig) (*Server, error) { srv := &Server{ postsDir: posts, Port: 8080, @@ -137,13 +133,8 @@ func New(logger *slog.Logger, posts fs.FS, opts config.ServerConfig) (*Server, e } } - // Precedence: WithLogger option > positional logger arg > slog.Default(). if srv.Logger.Logger == nil { - if logger != nil { - srv.Logger.Logger = logger - } else { - srv.Logger.Logger = slog.Default() - } + srv.Logger.Logger = slog.Default() } // Resolve the template filesystem. @@ -402,7 +393,7 @@ func (s *Server) refreshHandler(ctx context.Context) error { s.Logger.Logger.DebugContext(ctx, "Creating New Handler for Server") - handler := Handler(blog, nil, s.BlogRoot.AsOption(), s.Logger.AsOption(), s.AssetsDir.AsOption()) + handler := Handler(blog, s.BlogRoot.AsOption(), s.Logger.AsOption(), s.AssetsDir.AsOption()) // Apply middleware stack if configured if len(s.middleware) > 0 { diff --git a/pkg/server/swap_test.go b/pkg/server/swap_test.go index 1d0f778..8ec0a6c 100644 --- a/pkg/server/swap_test.go +++ b/pkg/server/swap_test.go @@ -57,7 +57,7 @@ func canonicalBody(t *testing.T, postsFS fs.FS) string { }, } - srv, err := server.New(nil, postsFS, cfg) + srv, err := server.New(postsFS, cfg) if err != nil { t.Fatalf("canonicalBody: server.New() error = %v", err) } @@ -103,7 +103,7 @@ func TestUpdatePosts_ConcurrentSwapIsAtomic(t *testing.T) { }, } - srv, err := server.New(nil, fsA, cfg) + srv, err := server.New(fsA, cfg) if err != nil { t.Fatalf("server.New() error = %v", err) } @@ -197,7 +197,7 @@ func TestUpdatePosts_ReloadFailureKeepsOldContent(t *testing.T) { }, } - srv, err := server.New(nil, fsA, cfg) + srv, err := server.New(fsA, cfg) if err != nil { t.Fatalf("server.New() error = %v", err) }