devserver

package
v0.1.121 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Overview

Package devserver is the Līdza dev server: the reverse proxy that puts the Go control plane and the frontend dev server behind one port, the static file server used in production, and the coordinator that keeps the app binary rebuilt while `lidza dev` runs.

Index

Constants

View Source
const (
	EnvMode        = "LIDZA_MODE"         // "dev" or unset
	EnvAddr        = "LIDZA_ADDR"         // listen address
	EnvFrontendURL = "LIDZA_FRONTEND_URL" // dev only: the dev server to proxy to
)

Environment the app binary reads, in dev (set by `lidza dev`) and in production.

View Source
const BuildDir = ".lidza"

BuildDir holds the dev build of the app binary, inside the project.

View Source
const EnvSSR = "LIDZA_SSR"

EnvSSR enables per-request rendering: "1" starts the sidecar.

View Source
const LocalesDir = ".locales"

LocalesDir is the directory inside dist where the prerender writes each page once per locale when the app has more than one (locales/*.json): .locales/<lang>/<path>/index.html, .locales/<lang>/shell.html for the paths it did not render, and .locales/manifest.json naming the locales and the default.

View Source
const MinCompress = 1024

MinCompress is the smallest file or page worth compressing: below it the headers outweigh the saving.

View Source
const SSRDir = ".server"

SSRDir is the directory inside dist that holds the server bundle and the sidecar script, written by the react template's build.

View Source
const ShellFile = ".server/index.html"

ShellFile is the unrendered index.html the react template's prerender keeps next to the server bundle; it serves every path that has no file of its own.

Variables

View Source
var PIDFile = filepath.Join(BuildDir, "dev.pid")

PIDFile, under the project, records the running `lidza dev`: its pid and the CLI version, so `lidza update` can say when one still runs the old CLI.

View Source
var RestartFile = filepath.Join(BuildDir, "restart")

RestartFile, under the project, restarts the app under `lidza dev` without a rebuild when its modification time changes. `lidza db migrate` and `rollback` touch it (RequestRestart), so the app does not keep running against the schema it started with.

Functions

func AgentFiles

func AgentFiles(next http.Handler) http.Handler

AgentFiles serves the files `lidza dev` keeps under .lidza for agents (the app's llms.txt, llms-full.txt and openapi.json) and passes every other request to next. Used in dev mode only.

They are always at /_lidza/llms.txt, /_lidza/llms-full.txt and /_lidza/openapi.json. At the root (/llms.txt ...) they are served only when the app has no file of its own there: a product's own /llms.txt for crawlers wins, and the framework's copy stays under /_lidza/.

func Compressible

func Compressible(ext string) bool

Compressible reports whether files with this extension are stored compressed and served by Accept-Encoding.

func Dev

func Dev(ctx context.Context, cfg *config.Config, opt Options) error

Dev runs the project in cfg.Dir until ctx is cancelled: starts the frontend dev server if the template has one, builds the app binary, runs it in dev mode, and rebuilds and restarts it whenever a Go source file changes.

func EnsureNodeModules

func EnsureNodeModules(ctx context.Context, dir string, out io.Writer) error

EnsureNodeModules runs `npm install` in dir when it has a package.json whose dependencies are not all in node_modules: a fresh clone, or a pull that added packages (another session's, a teammate's).

func HasSidecar

func HasSidecar(dist fs.FS) bool

HasSidecar reports whether dist carries the sidecar files.

func InjectHead

func InjectHead(page []byte, h Head) []byte

InjectHead writes h into page's <head>: the title replaced, the meta tags it sets replacing the page's of the same name or property, the canonical link replaced, the JSON-LD added. A page without a <head> is returned as it is.

func KeepDist

func KeepDist(dir, dist string) error

KeepDist restores dist/.gitkeep after a frontend build: the bundler empties the directory, and the Go embed of dist needs it to exist in a fresh clone, so the placeholder stays tracked.

func NewProxy

func NewProxy(target string, opts ...Option) (http.Handler, error)

NewProxy returns a handler that forwards every request to the frontend dev server at target, including WebSocket upgrades (Vite HMR). The Host header is rewritten to the target so Vite's allowed-hosts check passes. WithHead sets the head of the HTML pages it passes on, as Static does in production, and their inline scripts are hashed into the response's Content-Security-Policy.

func NewestSource

func NewestSource(dir string) time.Time

NewestSource is the latest modification time of the Go sources in dir (*.go, go.mod, go.sum), skipping what the dev watcher skips. `lidza mcp` compares it with its app build to rebuild a stale one.

func RemovePID

func RemovePID(dir string)

RemovePID removes the record when it is this process's.

func RequestRestart

func RequestRestart(dir string) error

RequestRestart asks a `lidza dev` running in dir to restart the app. It is a no-op in effect when none runs.

func RunningDev

func RunningDev(dir string) (pid int, version string, ok bool)

RunningDev reports the project's running dev server: its pid and CLI version. ok is false when none is recorded or its process is gone.

func Split

func Split(apiPrefix string, api, frontend http.Handler) http.Handler

Split routes requests under apiPrefix to api and everything else to frontend. apiPrefix is matched as a path prefix, "/api/" also matching "/api" itself.

func Static

func Static(dist fs.FS, opts ...Option) http.Handler

Static serves a built single-page app from dist: files by path (the .br or .gz copy lidza build stored when the request accepts it), hashed assets under /assets/ with a long cache lifetime, prerendered pages at <path>/index.html, and the shell for every other path (client-side routing). When the build has a page per locale (dist/.locales), pages and the shell come in the request's locale, negotiated as the i18n pack does (WithLocale passes the pack's own negotiation). WithHead sets each page's <head> per request.

func WritePID

func WritePID(dir, version string) error

WritePID records this process as the project's dev server.

Types

type Head struct {
	// Title replaces the page's <title>; og:title and twitter:title
	// follow it.
	Title string
	// Description replaces the page's meta description; og:description
	// and twitter:description follow it.
	Description string
	// Canonical is the page's absolute address: <link rel="canonical">
	// and og:url.
	Canonical string
	// Image is an absolute image address for og:image and twitter:image;
	// with one, twitter:card is summary_large_image, else summary.
	Image string
	// Type is og:type; "website" when empty.
	Type string
	// SiteName is og:site_name.
	SiteName string
	// NoIndex asks search engines not to index the page (robots
	// noindex): a draft, a private page.
	NoIndex bool
	// Meta are further <meta> tags (og:locale, article:published_time,
	// twitter:site); each replaces a tag of the page with the same name
	// or property.
	Meta []Meta
	// JSONLD are structured data objects (a map, a struct, a
	// json.RawMessage), each written as its own
	// <script type="application/ld+json">.
	JSONLD []any
	// Status is the response status when not zero: 404 for an address
	// whose record does not exist, so a crawler does not index an empty
	// page.
	Status int
}

Head is what a page's <head> says about it, decided on the server per request: the title, the description, the canonical address, the social cards (Open Graph and Twitter) and structured data (JSON-LD). A page with a path parameter is one shell for every address until the head hook (WithHead) names it; search engines and link previews read this, not what the browser renders later.

Every value is text: it is escaped where it is written, so a title taken from a database row cannot close a tag.

type HeadFunc

type HeadFunc func(r *http.Request) (Head, bool)

HeadFunc decides the head of the page r asks for; false leaves the page as built. It runs for every page request (not for assets), with the request's context carrying the app's services, so it may read the database; keep it to one query.

type LocaleFunc

type LocaleFunc func(r *http.Request) string

LocaleFunc names the locale a request asks for, such as the i18n pack's negotiation. An empty result leaves the choice to the built-in one.

type Meta

type Meta struct {
	Name     string
	Property string
	Content  string
}

Meta is one <meta> tag: Name (name="...") or Property (property="...", the Open Graph form), and Content.

type Option

type Option func(*options)

Option configures Static, NewSidecar and NewProxy.

func WithHead

func WithHead(f HeadFunc) Option

WithHead sets the head of every page Static, the sidecar and the dev proxy serve with f: the prerendered pages and the shell alike.

func WithLocale

func WithLocale(f LocaleFunc) Option

WithLocale picks the page variant with f instead of the built-in negotiation (?lang, the lang cookie, Accept-Language, the default).

type Options

type Options struct {
	// Addr is where the app listens; the frontend is proxied behind it.
	Addr string
	// Out receives the interleaved, prefixed output of every process.
	Out io.Writer
	// Poll is how often the Go sources are checked for changes.
	Poll time.Duration
	// BeforeBuild runs before every build. `lidza dev` uses it to run the
	// schema generators; an error is logged and the build still runs.
	BeforeBuild func() error
	// AfterBuild runs after every successful build of the app, before the
	// restart. `lidza dev` uses it to refresh the agent context files and
	// the generated client.
	AfterBuild func()
	// Watch lists extra files or directories, relative to the project
	// root, whose change triggers a rebuild besides the Go sources.
	Watch []string
	// Rewatch, when set, is called after every build for the new list of
	// extra files (lidza.json may have gained a pack whose sources are
	// now watched); it replaces Watch.
	Rewatch func() []string
}

Options configures a `lidza dev` run.

type Sidecar

type Sidecar struct {
	// contains filtered or unexported fields
}

Sidecar renders pages per request through a Node process running the app's SSR bundle, reached over a Unix socket. It is optional: without it, or when it fails, the prerendered or static page is served.

func NewSidecar

func NewSidecar(dist fs.FS, apiBase string, log *slog.Logger, opts ...Option) (*Sidecar, error)

NewSidecar prepares a sidecar for dist. apiBase is the app's own address, where loaders reach the API during a render. When the build has a page per locale (dist/.locales), each render is asked for the request's locale, negotiated as Static does.

func (*Sidecar) Handler

func (s *Sidecar) Handler(next http.Handler) http.Handler

Handler renders HTML page requests through the sidecar and passes everything else, and every failure, to next.

func (*Sidecar) Start

func (s *Sidecar) Start(ctx context.Context) error

Start extracts the bundle to a temporary directory and starts node.

func (*Sidecar) Stop

func (s *Sidecar) Stop(context.Context) error

Stop ends the process and removes the extracted files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL