NextMyDay/internal/modules/gen/main.go
Claus Lohmar d7d2c7d2b6 feat(module): add the module registry, discovery and the home module (SPEC-008)
- Build-time discovery (D-028, the Caddy pattern): a module self-registers from
  init(); internal/modules/imports_gen.go (go:generate) blank-imports every
  module directory, so THE DIRECTORY IS THE SOURCE OF TRUTH
- The registry: deterministic registration order, config-backed enable/disable
  (D-038), required modules, name-prefixed migrations, Mount() into the door
- The name-collision guard now also fails a module named for a reserved
  level-1 workflow (SPEC-009 §13 Q-1) — access · static · api · media · ws ·
  dev — derived from routing.ReservedWorkflows, `home` excepted (D-2)
- home: the reference dashboard module, and the core Modules page
2026-10-14 10:53:57 +00:00

83 lines
1.9 KiB
Go

// Command gendiscovery writes the module discovery aggregator
// (internal/modules/imports_gen.go) from the directories under
// internal/modules (D-028). It is invoked by `go generate ./internal/modules`.
//
// Usage: gendiscovery [dir] (dir defaults to ".")
package main
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
)
func main() {
dir := "."
if len(os.Args) > 1 {
dir = os.Args[1]
}
if err := run(dir); err != nil {
fmt.Fprintln(os.Stderr, "gendiscovery:", err)
os.Exit(1)
}
}
func run(dir string) error {
abs, err := filepath.Abs(dir)
if err != nil {
return err
}
root, modulePath, err := findModule(abs)
if err != nil {
return err
}
rel, err := filepath.Rel(root, abs)
if err != nil {
return err
}
importBase := path.Join(modulePath, filepath.ToSlash(rel))
src, err := Generate(dir, importBase)
if err != nil {
return err
}
out := filepath.Join(dir, "imports_gen.go")
if err := os.WriteFile(out, src, 0o644); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "gendiscovery: wrote", out)
return nil
}
// findModule walks up from dir to the nearest go.mod and returns its directory
// and module path.
func findModule(dir string) (root, modulePath string, err error) {
cur := dir
for {
candidate := filepath.Join(cur, "go.mod")
if data, readErr := os.ReadFile(candidate); readErr == nil {
if mp := parseModulePath(string(data)); mp != "" {
return cur, mp, nil
}
return "", "", fmt.Errorf("go.mod at %s declares no module path", candidate)
}
parent := filepath.Dir(cur)
if parent == cur {
return "", "", fmt.Errorf("no go.mod found above %s", dir)
}
cur = parent
}
}
// parseModulePath extracts the `module <path>` line from a go.mod.
func parseModulePath(goMod string) string {
for _, line := range strings.Split(goMod, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module "))
}
}
return ""
}