- 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
97 lines
2.5 KiB
Go
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
|
|
}
|