NextMyDay/internal/modules/gen/generate.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

97 lines
2.5 KiB
Go

package main
import (
"bytes"
"fmt"
"go/format"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
// Generate renders the discovery aggregator for the module directory dir. It
// blank-imports every sub-directory that holds Go source, in sorted order, so
// the aggregator is deterministic (SPEC-008 AC-2) and the DIRECTORY is the
// source of truth (D-028). importBase is the Go import path of dir.
func Generate(dir, importBase string) ([]byte, error) {
names, err := moduleDirs(dir)
if err != nil {
return nil, err
}
var buf bytes.Buffer
buf.WriteString("// Code generated by go generate; DO NOT EDIT.\n")
buf.WriteString("//\n")
buf.WriteString("// Produced by `go generate ./internal/modules` (internal/modules/generate.go).\n")
buf.WriteString("// Every sub-directory that holds Go source is blank-imported here so its\n")
buf.WriteString("// init() self-registration runs at startup (D-028): THE DIRECTORY IS THE\n")
buf.WriteString("// SOURCE OF TRUTH for the module set. Add a directory and re-run go generate.\n")
buf.WriteString("package modules\n\n")
if len(names) == 0 {
buf.WriteString("// No module directories were found.\n")
} else {
buf.WriteString("import (\n")
for _, n := range names {
fmt.Fprintf(&buf, "\t_ %q\n", path.Join(importBase, n))
}
buf.WriteString(")\n")
}
src, err := format.Source(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("gendiscovery: format output: %w", err)
}
return src, nil
}
// moduleDirs lists the module directories under dir, sorted.
func moduleDirs(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var names []string
for _, e := range entries {
if !e.IsDir() || skipDir(e.Name()) {
continue
}
ok, err := hasGoSource(filepath.Join(dir, e.Name()))
if err != nil {
return nil, err
}
if ok {
names = append(names, e.Name())
}
}
sort.Strings(names)
return names, nil
}
// skipDir reports whether a directory is tooling, not a module.
func skipDir(name string) bool {
switch name {
case "gen", "testdata":
return true
}
return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_")
}
// hasGoSource reports whether dir holds at least one non-test Go file.
func hasGoSource(dir string) (bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return false, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
n := e.Name()
if strings.HasSuffix(n, ".go") && !strings.HasSuffix(n, "_test.go") {
return true, nil
}
}
return false, nil
}