package main import ( "context" "fmt" "os" "os/signal" "sync" "syscall" "git.golem15.com/golem15/summercms/internal/docsite" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/conga" "git.golem15.com/golem15/summercms/modules/flare" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo" "git.golem15.com/golem15/summercms/modules/surf" ) func docsBuildCommand() bonfire.Command { return bonfire.Command{ Name: "docs:build", Description: "Build the documentation site", Long: `Checks every page under docs/ and every module README, then writes the static site to site/ (or --out): HTML pages, a raw .md copy of each page, llms.txt, llms-full.txt and the search index. It fails, writing nothing, when a page names an identifier that does not exist, links to a missing page or anchor, shows a command neither summer nor an application binary has, or has a src= code block that differs from its source. --check only reports. Run it from the framework root.`, Examples: []bonfire.Example{ {Command: "docs:build --check", Description: "Check the docs and write nothing"}, {Command: "docs:build --base-url /docs/ --out /tmp/site", Description: "Build the site for /docs/ into /tmp/site"}, }, Flags: []bonfire.Flag{ {Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."}, {Name: "src", Description: "Docs source directory (default /docs)"}, {Name: "out", Description: "Output directory (default /site)"}, {Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"}, {Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"}, {Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"}, {Name: "check", Description: "Validate the docs and write nothing", Bare: true}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { opts := docsOptions(in) if flagTrue(in, "check") { problems, err := docsite.Check(opts) if err != nil { return err } if len(problems) > 0 { return reportDocsProblems(out, "docs:build", problems) } out.Printf("docs:build: no problems found\n") return nil } result, problems, err := docsite.Build(opts) if err != nil { return err } if len(problems) > 0 { return reportDocsProblems(out, "docs:build", problems) } out.Printf("docs:build: wrote %d pages to %s\n", result.Pages, result.Out) return nil }, } } func docsSyncCommand() bonfire.Command { return bonfire.Command{ Name: "docs:sync", Description: "Rewrite src= code blocks from their sources", Long: `Rewrites every code block that has a src= reference with the current text of its source (a file, or a function, test or example in it), so the docs show what the code does. Run it after changing code a page shows; docs:build --check fails until you do.`, Examples: []bonfire.Example{ {Command: "docs:sync", Description: "Refresh every src= block of the docs in this repository"}, {Command: "docs:sync --root ../summercms.go", Description: "Refresh the docs of another checkout"}, }, Flags: []bonfire.Flag{ {Name: "root", Description: "Repository root; src= paths resolve against it", Default: "."}, {Name: "src", Description: "Docs source directory (default /docs)"}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { result, problems, err := docsite.Sync(docsOptions(in)) if err != nil { return err } if len(problems) > 0 { return reportDocsProblems(out, "docs:sync", problems) } if result.Snippets == 0 { out.Printf("docs:sync: all snippets up to date\n") return nil } out.Printf("docs:sync: updated %d snippets in %d files\n", result.Snippets, result.Files) return nil }, } } func docsServeCommand() bonfire.Command { return bonfire.Command{ Name: "docs:serve", Description: "Build the documentation site and preview it on a local address", Long: `Builds the documentation site like docs:build into a temporary directory and serves it on a loopback address. It watches the docs sources and the files their src= references point to, and rebuilds on change; a failed rebuild keeps the last good site. Ctrl+C stops the server and exits 0.`, Examples: []bonfire.Example{ {Command: "docs:serve", Description: "Preview the docs of the current repository on " + docsite.DefaultServeAddr}, {Command: "docs:serve --addr 127.0.0.1:4100", Description: "Use another port"}, }, Flags: []bonfire.Flag{ {Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."}, {Name: "src", Description: "Docs source directory (default /docs)"}, {Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"}, {Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"}, {Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"}, {Name: "addr", Description: "Listen address; must be loopback unless --allow-remote", Default: docsite.DefaultServeAddr}, {Name: "allow-remote", Description: "Allow a non-loopback --addr (serves the docs on the network)", Bare: true}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { addr, _ := in.Flag("addr") if addr == "" { addr = docsite.DefaultServeAddr } ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() return docsite.Serve(ctx, docsOptions(in), addr, flagTrue(in, "allow-remote"), outputWriter{mu: &sync.Mutex{}, out: out}) }, } } // outputWriter adapts bonfire.Output to io.Writer for docsite.Serve, which // writes from its watch goroutine too. type outputWriter struct { mu *sync.Mutex out bonfire.Output } func (w outputWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() w.out.Printf("%s", p) return len(p), nil } func docsOptions(in bonfire.Input) docsite.Options { opts := docsite.Options{Commands: docsCommands()} opts.Root, _ = in.Flag("root") opts.Src, _ = in.Flag("src") opts.Out, _ = in.Flag("out") opts.BaseURL, _ = in.Flag("base-url") opts.SiteURL, _ = in.Flag("site-url") opts.SiteLabel, _ = in.Flag("site-label") return opts } // docsCommands collects the command names docs pages may show: the summer // tool's own commands, list and help (added by bonfire.NewRoot), and every // command the generated application main // registers (TestDocsCommandsMirrorGeneratedMain keeps this list in step // with internal/build) plus the realtime and push commands applications // append. The constructors only capture the app, so an empty config is // enough; nothing runs. func docsCommands() *docsite.Commands { // list and help come from bonfire.NewRoot, in both programs. tool := []string{"list", "help"} for _, c := range toolCommands() { tool = append(tool, c.Name) } app := backpack.New(&compass.Config{}) var appCmds []bonfire.Command appCmds = append(appCmds, lagoon.RuntimeCommands(app, nil)...) appCmds = append(appCmds, lagoon.KeyGenerateCommand()) appCmds = append(appCmds, conga.RuntimeCommands(app, nil)...) appCmds = append(appCmds, surf.ServeCommand(app, nil)) appCmds = append(appCmds, surf.RouteListCommand(app, nil)) appCmds = append(appCmds, cabana.RuntimeCommands(app)...) appCmds = append(appCmds, centrifugo.Commands(app)...) appCmds = append(appCmds, flare.Commands(app)...) names := []string{"list", "help"} for _, c := range appCmds { names = append(names, c.Name) } return &docsite.Commands{Tool: tool, App: names} } // reportDocsProblems prints one line per problem and a summary, and returns // a Reported error, so the binary exits 1 without repeating the list. func reportDocsProblems(out bonfire.Output, cmd string, problems []docsite.Problem) error { for _, p := range problems { out.Printf("%s\n", p) } out.Printf("%s: %d problems, nothing written\n", cmd, len(problems)) return bonfire.Reported(fmt.Errorf("%s failed", cmd)) }