Compare commits

...

4 Commits

Author SHA1 Message Date
8e3a9f2cbd actualize claude instructions 2026-05-26 19:33:33 +00:00
1479022388 makefile: fix missing .PHONY 2026-05-14 22:17:55 +00:00
fd9621669c Makefile: fix help output and annotated release tags
- fix `make help` printing "Makefile" instead of target names — caused by
  `include tools.versions` adding to MAKEFILE_LIST; grep now targets Makefile directly
- `make release` now creates annotated tags: pre-populates the message
  with commits since the previous tag and opens $EDITOR for review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:53:21 +00:00
5055d69685 pkg/logger and pkg/testutil 2026-05-14 21:45:17 +00:00
13 changed files with 196 additions and 93 deletions

View File

@ -0,0 +1,28 @@
Scaffold a new internal domain package named `$ARGUMENTS`.
## Steps
1. Create `internal/$ARGUMENTS/$ARGUMENTS.go`:
- Package declaration `package $ARGUMENTS`
- One exported concrete type named after the package's responsibility (e.g. `Service`, `Store`, `Client`, `Parser`)
- Constructor: `func New(...) *<Type>` — only accept dependencies the type actually needs; leave the signature empty if none are obvious yet
- At least one exported method stub representing the package's primary operation; return `result.Expect[T]` if the operation can fail
- Doc comment on every exported symbol (the linter enforces this)
2. Create `internal/$ARGUMENTS/$ARGUMENTS_test.go`:
- Package: `package $ARGUMENTS_test` (black-box)
- Declare a minimal interface covering only the methods the test calls
- Write a manual fake struct that satisfies that interface (no code generation)
- One table-driven test using `t.Run` and helpers from `gitea.djmil.dev/go/template/pkg/testutil`
- Use `logger.NewNop()` from `gitea.djmil.dev/go/template/pkg/logger` if logging is needed
## Rules (do not break these)
- Never define an interface inside the package itself — consumers define interfaces
- Never call `.Expect()`, `.Must()`, or `.Expectf()` inside the package — only return `result.Expect[T]`
- No third-party imports
- No hard-coded configuration values
## After scaffolding
Run `make lint test` to verify the files compile and the stub test passes. Report what was created and suggest what the caller should wire in `cmd/app/main.go`.

8
.claude/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(go test *)",
"Bash(go vet *)"
]
}
}

View File

@ -8,25 +8,41 @@ Keep it concise — the agent needs signal, not essays.
## Project overview ## Project overview
Go 1.25 template for PoC, hobby projects, and small publishable packages. Go 1.25 template for PoC, hobby projects, and small publishable packages.
Demonstrates: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, Stack: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes,
result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck), result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck).
git hooks, devcontainer, VSCode tasks.
Key constraint: `go.mod` stays free of dev tool deps (tools are pinned in `tools.versions` and run via Key constraint: `go.mod` stays free of dev tool deps (tools are pinned in `tools.versions` and run via
`go run tool@version`) so packages published from this repo have a clean module graph for consumers. `go run tool@version`) so packages published from this repo have a clean module graph for consumers.
Module: `gitea.djmil.dev/go/template` — update this when you fork. ---
## Design philosophy
**[The Twelve-Factor App](https://12factor.net) is the primary reference for default implementation choices.**
Follow its recommendations unless circumstances force a deviation — and document the reason when you do.
Key factors that shape this codebase most directly:
- **[Logs (factor XI)](https://12factor.net/logs)** — the app writes log events to `stderr` as an unbuffered stream.
It never opens log files or manages rotation. The execution environment (shell, systemd, Docker, k8s)
routes and stores the stream. Human-readable program output goes to `stdout` (`fmt.Print*`).
- **[Config (factor III)](https://12factor.net/config)** — all configuration comes from the environment
(flags in this template; env vars are the 12-factor ideal). No hard-coded values in logic packages.
- **[Dependencies (factor II)](https://12factor.net/dependencies)** — explicit declaration; dev tools are
pinned in `tools.versions` and kept out of `go.mod` so published packages have a clean module graph.
--- ---
## Project structure ## Project structure
``` ```
cmd/app/main.go composition root — wires deps, no logic here cmd/app/ CLI entrypoint: parses flags, wires dependencies, and expresses
internal/config/ flag-based config loader (config.Load) what the program does in high-level readable steps — calls internal/
internal/logger/ slog wrapper with WithField / WithFields and pkg/ packages; no domain logic lives here
internal/greeter/ Example domain package (delete or repurpose) internal/ domain-specific packages; logical grouping of substantial code into
pkg/result/ Example publishable package (Result/Expect types) digestible types and interfaces; not importable outside this module
pkg/ generic, publishable packages reusable across projects; no
assumptions about the calling application
tools.versions Pinned tool versions (sourced by Makefile and pre-push hook) tools.versions Pinned tool versions (sourced by Makefile and pre-push hook)
.golangci.yml Linter rules .golangci.yml Linter rules
.githooks/pre-push Runs gofmt + go vet + golangci-lint + gosec before push .githooks/pre-push Runs gofmt + go vet + golangci-lint + gosec before push
@ -36,8 +52,8 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
## Project rules ## Project rules
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...` - **Module imports** — always use the full module path from `go.mod` (never relative imports)
- **Packages**keep `cmd/` thin (wiring only); business logic belongs in `internal/` - **Packages**`cmd/` owns CLI parsing, dependency wiring, and high-level orchestration; domain logic belongs in `internal/`
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free) - **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
- **Errors**`pkg/result` is a convenience tool for removing error-threading clutter from application logic; use it as follows: - **Errors**`pkg/result` is a convenience tool for removing error-threading clutter from application logic; use it as follows:
- `pkg/` libraries **only return** `result.Expect[T]` — never call `.Expect()`, `.Must()`, or `.Expectf()` inside library code; those methods exit the goroutine via `runtime.Goexit` and are only safe in application-layer code protected by a boundary - `pkg/` libraries **only return** `result.Expect[T]` — never call `.Expect()`, `.Must()`, or `.Expectf()` inside library code; those methods exit the goroutine via `runtime.Goexit` and are only safe in application-layer code protected by a boundary
@ -47,8 +63,8 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")` - bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error - use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail` - still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail`
- **Logging** — use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend - **Logging**logs go to `stderr` (structured, machine-readable, per 12-factor XI); human output goes to `stdout` via `fmt.Print*`. Use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend
- **Config** — all configuration through `internal/config` (flag-parsed); no hard-coded values in logic packages - **Config** — all configuration parsed in `cmd/app/config.go` (flags); no hard-coded values in `internal/` or `pkg/` packages
--- ---
@ -60,7 +76,6 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
- Constructors: `New(deps...) *Type` pattern - Constructors: `New(deps...) *Type` pattern
- Comment every exported symbol (golangci-lint will warn if missing) - Comment every exported symbol (golangci-lint will warn if missing)
- Max line length: 120 chars (configured in `.golangci.yml`) - Max line length: 120 chars (configured in `.golangci.yml`)
- Prefer explicit over clever; PoC code should be readable first
--- ---
@ -70,10 +85,11 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
- Test files: `package foo_test` (black-box) unless white-box access is needed - Test files: `package foo_test` (black-box) unless white-box access is needed
- Fake dependencies with **manual fakes** (implement the interface inline in `_test.go`) - Fake dependencies with **manual fakes** (implement the interface inline in `_test.go`)
- Use `logger.NewNop()` when the test doesn't care about log output - Use `logger.NewNop()` when the test doesn't care about log output
- Use `logger.NewWriter(&buf, "debug")` in acceptance / integration tests that need to assert on log content
- Table-driven tests with `t.Run("description", ...)` for multiple cases - Table-driven tests with `t.Run("description", ...)` for multiple cases
- The race detector is enabled in CI (`make test-race`); don't introduce data races - The race detector is enabled in CI (`make test-race`); don't introduce data races
- Never use `time.Sleep` in tests; use channels or `t.Cleanup` - Never use `time.Sleep` in tests; use channels or `t.Cleanup`
- Use `internal/testutil` helpers instead of manual checks — `ResultOk`, `ResultOkNotNil`, `ResultErr` for `result.Expect[T]`; `NoError`, `Error`, `ErrorContains`, `Equal` for plain values - Use `gitea.djmil.dev/go/template/pkg/testutil` helpers instead of manual checks — `ResultOk`, `ResultOkNotNil`, `ResultErr` for `result.Expect[T]`; `NoError`, `Error`, `ErrorContains`, `Equal` for plain values
--- ---
@ -93,9 +109,6 @@ make release # list releases, or tag+push after full checks (make release
make clean # remove bin/ make clean # remove bin/
``` ```
VSCode: `Ctrl+Shift+B` = build, `Ctrl+Shift+T` = test.
Debug: use launch config "Debug: app" (F5).
--- ---
## Adding new features (checklist) ## Adding new features (checklist)

View File

@ -1,4 +1,4 @@
.PHONY: help init setup build run test test-race lint lint-fix security docs clean .PHONY: help init setup build run test test-race test-verbose lint lint-fix security docs release clean
include tools.versions include tools.versions
@ -9,7 +9,7 @@ CMD_PATH := ./cmd/app
# ── Default target ───────────────────────────────────────────────────────────── # ── Default target ─────────────────────────────────────────────────────────────
help: ## Show this help message help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' | sort awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' | sort
# ── First-time setup ─────────────────────────────────────────────────────────── # ── First-time setup ───────────────────────────────────────────────────────────
@ -82,7 +82,18 @@ ifdef VERSION
$(MAKE) test-race $(MAKE) test-race
$(MAKE) lint $(MAKE) lint
$(MAKE) security $(MAKE) security
git tag $(VERSION) @MSG=$$(mktemp); \
LAST=$$(git describe --tags --abbrev=0 2>/dev/null); \
echo "$(VERSION)" > $$MSG; \
echo "" >> $$MSG; \
if [ -n "$$LAST" ]; then \
echo "Changes since $$LAST:" >> $$MSG; \
git log $$LAST..HEAD --oneline >> $$MSG; \
else \
git log --oneline >> $$MSG; \
fi; \
git tag -a -e -F $$MSG $(VERSION); \
rm -f $$MSG
git push origin $(VERSION) git push origin $(VERSION)
else else
@echo "Released versions:" @echo "Released versions:"

View File

@ -74,6 +74,16 @@ make release # list tags, or run full checks then tag+push (make release V
> **Keyboard shortcut (VSCode):** `Ctrl+Shift+B` → build, `Ctrl+Shift+T` → test. > **Keyboard shortcut (VSCode):** `Ctrl+Shift+B` → build, `Ctrl+Shift+T` → test.
### Claude Code commands
If you use [Claude Code](https://claude.ai/code), the repo ships a custom slash command:
| Command | What it does |
|---|---|
| `/new-package <name>` | Scaffolds `internal/<name>/` with a concrete type, constructor, doc comments, and a black-box test file — all wired to project conventions |
Commands live in `.claude/commands/` and are available to anyone who clones the repo.
--- ---
## Project structure ## Project structure

View File

@ -1,13 +1,4 @@
// Package config parses application configuration from command-line flags. package main
// Defaults are defined here; override at runtime with flags:
//
// ./app -port 9090 -env prod -log-level warn
//
// Usage:
//
// cfg := config.Load()
// fmt.Println(cfg.App.Port)
package config
import ( import (
"flag" "flag"
@ -36,9 +27,16 @@ type GreeterConfig struct {
Name string Name string
} }
// Load parses command-line flags and returns a Config. // parseArgs parses application configuration from command-line flags.
// Call this once at startup before any other flag parsing. // Defaults are defined here; override at runtime with flags:
func Load() *Config { //
// ./app -port 9090 -env prod -log-level warn 2 > log.log
//
// Usage:
//
// cfg := config.parseArgs()
// fmt.Println(cfg.App.Port)
func parseArgs() *Config {
name := flag.String("name", "Gopher", "application name") name := flag.String("name", "Gopher", "application name")
port := flag.Int("port", 8080, "listen port") port := flag.Int("port", 8080, "listen port")
env := flag.String("env", "dev", "environment: dev | staging | prod") env := flag.String("env", "dev", "environment: dev | staging | prod")

View File

@ -1,6 +1,7 @@
// main is the composition root for the application. // main is the composition root for the application.
// It wires together config, logger, and domain services — nothing more. // It parses config, wires dependencies into an app struct, and delegates.
// Business logic lives in internal/; cmd/ is deliberately thin. // Implementation details lives in internal/; cmd/ should be kept thin by
// deliberately operating only with high level concepts.
package main package main
import ( import (
@ -8,14 +9,42 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"gitea.djmil.dev/go/template/internal/config"
"gitea.djmil.dev/go/template/internal/greeter" "gitea.djmil.dev/go/template/internal/greeter"
"gitea.djmil.dev/go/template/internal/logger" "gitea.djmil.dev/go/template/pkg/logger"
"gitea.djmil.dev/go/template/pkg/result" "gitea.djmil.dev/go/template/pkg/result"
) )
// app holds all wired dependencies for the lifetime of the process.
type app struct {
cfg *Config
log *logger.Logger
greeter *greeter.Service
}
func newApp(cfg *Config) *app {
var log *logger.Logger
if cfg.App.Env == "dev" {
log = logger.NewDevelopment()
} else {
log = logger.New(cfg.Logger.Level).Expect("create logger") // might fail dramatically
}
return &app{
cfg: cfg,
log: log,
greeter: greeter.New(log),
}
}
func main() { func main() {
if err := result.Run(showGreeting); err != nil { conf := parseArgs()
app := newApp(conf)
app.log.WithFields(map[string]any{
"app": filepath.Base(os.Args[0]),
"env": conf.App.Env,
}).Info("starting up")
if err := result.Run(app.run); err != nil {
fmt.Fprintf(os.Stderr, "[failed] %v\n", err) fmt.Fprintf(os.Stderr, "[failed] %v\n", err)
if stack := result.StackTrace(err); stack != "" { if stack := result.StackTrace(err); stack != "" {
fmt.Fprintf(os.Stderr, "%s\n", stack) fmt.Fprintf(os.Stderr, "%s\n", stack)
@ -24,30 +53,17 @@ func main() {
} }
} }
func showGreeting() { func (a *app) run() {
// ── Config ──────────────────────────────────────────────────────────────── // High level business logic goes here
cfg := config.Load() a.showGreeting(a.cfg.Greeter.Name)
// ── Logger ──────────────────────────────────────────────────────────────── // Human readable messages.
var log *logger.Logger // Use logs for presenting technical data in machine friendly format.
if cfg.App.Env == "dev" { fmt.Printf("TODO: implement listening on port %d\n", a.cfg.App.Port)
log = logger.NewDevelopment() }
} else {
log = logger.New(cfg.Logger.Level).Expect("create logger") func (a *app) showGreeting(name string) {
} msg := a.greeter.Greet(name).Expect("greeting")
a.log.WithField("message", msg).Info("greeting complete")
log.WithFields(map[string]any{ fmt.Println(msg)
"app": filepath.Base(os.Args[0]),
"env": cfg.App.Env,
}).Info("starting up")
// ── Services ──────────────────────────────────────────────────────────────
greetSvc := greeter.New(log)
// ── Example usage ─────────────────────────────────────────────────────────
msg := greetSvc.Greet(cfg.Greeter.Name).Expect("greeting")
log.WithField("message", msg).Info("greeting complete")
fmt.Printf("%s (listening on :%d)\n", msg, cfg.App.Port)
} }

View File

@ -1,8 +1,7 @@
// Package greeter is a minimal example domain package. // Package greeter is a minimal example domain package.
// It demonstrates how to: // It demonstrates how to:
// - define an interface (satisfied by manual fakes in tests) // - define an interface (satisfied by manual fakes in tests)
// - inject dependencies (logger) through a constructor // - inject a component-scoped logger through the constructor
// - use the logger.WithField pattern
// //
// Replace this package with your own domain logic. // Replace this package with your own domain logic.
package greeter package greeter
@ -10,7 +9,7 @@ package greeter
import ( import (
"fmt" "fmt"
"gitea.djmil.dev/go/template/internal/logger" "gitea.djmil.dev/go/template/pkg/logger"
"gitea.djmil.dev/go/template/pkg/result" "gitea.djmil.dev/go/template/pkg/result"
) )
@ -21,7 +20,7 @@ type Service struct {
// New creates a Greeter service with the provided logger. // New creates a Greeter service with the provided logger.
func New(log *logger.Logger) *Service { func New(log *logger.Logger) *Service {
return &Service{log: log} return &Service{log: log.WithField("component", "greeter")}
} }
// Greet returns a personalized greeting and logs the interaction. // Greet returns a personalized greeting and logs the interaction.
@ -32,10 +31,7 @@ func (s *Service) Greet(name string) result.Expect[string] {
msg := fmt.Sprintf("Hello, %s!", name) msg := fmt.Sprintf("Hello, %s!", name)
s.log. s.log.WithField("name", name).Debug("greeting generated")
WithField("component", "greeter").
WithField("name", name).
Info("greeting generated")
return result.Ok(msg) return result.Ok(msg)
} }

View File

@ -4,9 +4,9 @@ import (
"testing" "testing"
"gitea.djmil.dev/go/template/internal/greeter" "gitea.djmil.dev/go/template/internal/greeter"
"gitea.djmil.dev/go/template/internal/logger" "gitea.djmil.dev/go/template/pkg/logger"
"gitea.djmil.dev/go/template/internal/testutil"
"gitea.djmil.dev/go/template/pkg/result" "gitea.djmil.dev/go/template/pkg/result"
"gitea.djmil.dev/go/template/pkg/testutil"
) )
// ── Service (unit tests) ────────────────────────────────────────────────────── // ── Service (unit tests) ──────────────────────────────────────────────────────

View File

@ -1,16 +1,26 @@
// Package logger wraps log/slog with a thin, ergonomic API. // Package logger wraps log/slog with a thin, ergonomic API.
// //
// The key addition over raw slog is the WithField / WithFields helpers that // Per the Twelve-Factor App (factor XI), the application writes structured log
// return a *Logger (not a *slog.Logger), so callers stay in the typed world // events to stderr and never manages log files or routing itself. The execution
// and can chain field attachments without importing slog directly. // environment (shell, systemd, Docker) is responsible for capturing and storing
// the stream. Human-readable output belongs on stdout via fmt.Print*.
// //
// Usage: // Typical use:
// //
// log := logger.New("info").Expect("create logger") // log := logger.New("info").Expect("create logger")
// log.Info("server started") // log.Info("server started", "port", 8080)
// //
// req := log.WithField("request_id", rid).WithField("user_id", uid) // // child logger for request-scoped fields that repeat across many lines:
// req.Info("handling request") // req := log.WithField("request_id", rid)
// req.Info("start")
// req.Info("end")
//
// In tests that need to assert on log content, use NewWriter with a buffer:
//
// var buf bytes.Buffer
// log := logger.NewWriter(&buf, "debug").Expect("create logger")
// // ... exercise code ...
// // assert on buf.String()
package logger package logger
import ( import (
@ -41,7 +51,7 @@ func New(level string) result.Expect[*Logger] {
return result.Ok(logger) return result.Ok(logger)
} }
// NewDevelopment creates a human-friendly text logger writing to stderr. // NewDevelopment creates a human-friendly text logger writing to stderr at debug level.
// Use this in local dev; prefer New() in any deployed environment. // Use this in local dev; prefer New() in any deployed environment.
func NewDevelopment() *Logger { func NewDevelopment() *Logger {
h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})
@ -49,6 +59,19 @@ func NewDevelopment() *Logger {
return &Logger{slog.New(h)} return &Logger{slog.New(h)}
} }
// NewWriter creates a JSON logger writing to w. Intended for tests that need to
// assert on log content — pass a *bytes.Buffer and inspect it after the fact.
func NewWriter(w io.Writer, level string) result.Expect[*Logger] {
lvl := parseLevel(level)
if lvl.Err() != nil {
return result.Errw[*Logger](lvl.Err(), "parse log level")
}
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: lvl.Value()})
return result.Ok(&Logger{slog.New(handler)})
}
// NewNop returns a no-op logger. Useful in tests that don't care about logs. // NewNop returns a no-op logger. Useful in tests that don't care about logs.
func NewNop() *Logger { func NewNop() *Logger {
return &Logger{slog.New(slog.NewTextHandler(io.Discard, nil))} return &Logger{slog.New(slog.NewTextHandler(io.Discard, nil))}

View File

@ -3,8 +3,8 @@ package logger_test
import ( import (
"testing" "testing"
"gitea.djmil.dev/go/template/internal/logger" "gitea.djmil.dev/go/template/pkg/logger"
"gitea.djmil.dev/go/template/internal/testutil" "gitea.djmil.dev/go/template/pkg/testutil"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {

View File

@ -1,5 +1,5 @@
// Package testutil provides lightweight test helpers to reduce boilerplate in // Package testutil provides lightweight test helpers to reduce boilerplate in
// table-driven tests. Import it from any _test.go file in this module. // table-driven tests.
// //
// Every helper calls t.Helper() so failures are reported at the call site, not // Every helper calls t.Helper() so failures are reported at the call site, not
// inside this package. // inside this package.

View File

@ -10,7 +10,7 @@
# **/*.go import paths # **/*.go import paths
# .devcontainer/devcontainer.json name field # .devcontainer/devcontainer.json name field
# README.md heading + module path references # README.md heading + module path references
# CLAUDE.md Module line # CLAUDE.md internal import references (pkg/* preserved)
# .golangci.yml goimports local-prefixes # .golangci.yml goimports local-prefixes
# git tags all template tags deleted # git tags all template tags deleted
# git history squashed into one INIT commit # git history squashed into one INIT commit
@ -126,17 +126,17 @@ sedi() {
fi fi
} }
# ── Helper: rename module path in a file, preserving pkg/result imports ─────── # ── Helper: rename module path in a file, preserving all pkg/* imports ────────
# pkg/result is a standalone publishable package; its import path must not # pkg/ packages are standalone publishable packages from this template repo;
# change when the consuming project is renamed. # their import paths must not change when a consuming project is renamed.
RESULT_PKG="${OLD_MODULE}/pkg/result" PKG_BASE="${OLD_MODULE}/pkg/"
PLACEHOLDER="__RESULT_PKG_PLACEHOLDER__" PLACEHOLDER="__TEMPLATE_PKG_BASE__"
rename_module_in() { rename_module_in() {
local file="$1" local file="$1"
sedi "s|${RESULT_PKG}|${PLACEHOLDER}|g" "$file" sedi "s|${PKG_BASE}|${PLACEHOLDER}|g" "$file"
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$file" sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$file"
sedi "s|${PLACEHOLDER}|${RESULT_PKG}|g" "$file" sedi "s|${PLACEHOLDER}|${PKG_BASE}|g" "$file"
} }
# ── Apply substitutions ─────────────────────────────────────────────────────── # ── Apply substitutions ───────────────────────────────────────────────────────