server API

server

package

API reference for the server package.

F
function

TestIncrementPort

TestIncrementPort verifies port arithmetic.

Parameters

cmd/rfw/server/server_test.go:13-17
func TestIncrementPort(t *testing.T)

{
	if got := incrementPort("8080"); got != "8081" {
		t.Fatalf("expected 8081, got %s", got)
	}
}
F
function

TestReadBuildType

TestReadBuildType reads build type from temporary manifest.

Parameters

cmd/rfw/server/server_test.go:20-37
func TestReadBuildType(t *testing.T)

{
	dir := t.TempDir()
	t.Chdir(dir)

	// No file should return empty string.
	if got := readBuildType(); got != "" {
		t.Fatalf("expected empty build type, got %q", got)
	}

	// Create manifest and verify value.
	data := []byte(`{"build":{"type":"SSC"}}`)
	if err := os.WriteFile(filepath.Join(dir, "rfw.json"), data, 0o600); err != nil {
		t.Fatalf("write manifest: %v", err)
	}
	if got := readBuildType(); got != "ssc" {
		t.Fatalf("expected 'ssc', got %q", got)
	}
}
F
function

TestIsGenerated

TestIsGenerated ensures generated files are skipped.

Parameters

cmd/rfw/server/server_test.go:40-52
func TestIsGenerated(t *testing.T)

{
	tests := map[string]bool{
		"rfw_devtools.go":       true,
		"some/rfw_generated.go": true,
		"rfw.go":                false,
		"cmd/rfw/server.go":     false,
	}
	for path, want := range tests {
		if got := isGenerated(path); got != want {
			t.Fatalf("isGenerated(%q) = %v, want %v", path, got, want)
		}
	}
}
F
function

TestShouldIgnore

TestShouldIgnore verifies the ignore window logic.

Parameters

cmd/rfw/server/server_test.go:55-64
func TestShouldIgnore(t *testing.T)

{
	s := &Server{}
	if s.shouldIgnore(time.Now()) {
		t.Fatalf("expected no ignore by default")
	}
	s.ignoreUntil = time.Now().Add(time.Second)
	if !s.shouldIgnore(time.Now()) {
		t.Fatalf("expected ignore within window")
	}
}
F
function

TestDebouncerCoalescesBursts

A burst of triggers within the window must coalesce into a single fire.

Parameters

cmd/rfw/server/debounce_test.go:14-37
func TestDebouncerCoalescesBursts(t *testing.T)

{
	var d debouncer
	for i := 0; i < 5; i++ {
		d.trigger(30 * time.Millisecond)
		time.Sleep(5 * time.Millisecond)
	}
	select {
	case <-d.C:
		d.fired()
	case <-time.After(time.Second):
		t.Fatalf("debouncer never fired")
	}
	if d.C != nil {
		t.Fatalf("channel not cleared after fire")
	}
	// No second fire is pending.
	d.trigger(20 * time.Millisecond)
	select {
	case <-d.C:
		d.fired()
	case <-time.After(time.Second):
		t.Fatalf("debouncer did not re-arm")
	}
}
F
function

TestStopHostGraceful

stopHost must terminate the child gracefully: a process that honours
SIGTERM exits without being killed, well before the kill timeout.

Parameters

cmd/rfw/server/debounce_test.go:41-58
func TestStopHostGraceful(t *testing.T)

{
	if runtime.GOOS == "windows" {
		t.Skip("SIGTERM not supported on windows")
	}
	s := NewServer("0", false)
	s.hostCmd = exec.Command("sh", "-c", `trap 'exit 0' TERM; sleep 30 & wait`)
	if err := s.hostCmd.Start(); err != nil {
		t.Fatalf("start: %v", err)
	}
	start := time.Now()
	s.stopHost()
	if elapsed := time.Since(start); elapsed >= hostStopTimeout {
		t.Fatalf("stopHost fell back to kill after %s", elapsed)
	}
	if s.hostCmd != nil {
		t.Fatalf("hostCmd not cleared")
	}
}
S
struct

RebuildEvent

RebuildEvent is emitted when a file change triggers a rebuild.

cmd/rfw/server/events.go:8-12
type RebuildEvent struct

Fields

Name Type Description
Path string
Success bool
Error string
S
struct

devMessage

cmd/rfw/server/hmr.go:15-20
type devMessage struct

Fields

Name Type Description
Type string json:"type"
Path string json:"path,omitempty"
Component string json:"component,omitempty"
Markup string json:"markup,omitempty"
F
function

componentNamesForTemplate

Parameters

templatePath
string

Returns

[]string
cmd/rfw/server/hmr_template.go:15-57
func componentNamesForTemplate(templatePath string) []string

{
	abs := templatePath
	if v, err := filepath.Abs(templatePath); err == nil {
		abs = v
	}
	dir := filepath.Dir(abs)
	if filepath.Base(dir) == "templates" {
		dir = filepath.Dir(dir)
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}
	rel, err := filepath.Rel(dir, abs)
	if err != nil {
		rel = filepath.Base(abs)
	}
	rel = filepath.ToSlash(rel)
	var names []string
	seen := map[string]struct{}{}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
			continue
		}
		goFile := filepath.Join(dir, entry.Name())
		fileNames := embedVariablesForTemplate(goFile, rel)
		if len(fileNames) == 0 {
			continue
		}
		comps := componentsUsingTemplates(goFile, fileNames)
		for _, name := range comps {
			if _, ok := seen[name]; ok {
				continue
			}
			seen[name] = struct{}{}
			names = append(names, name)
		}
	}
	if len(names) == 0 {
		return nil
	}
	return names
}
F
function

embedVariablesForTemplate

Parameters

goFile
string
rel
string

Returns

map[string]struct{}
cmd/rfw/server/hmr_template.go:59-85
func embedVariablesForTemplate(goFile, rel string) map[string]struct{}

{
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, goFile, nil, parser.ParseComments)
	if err != nil {
		return nil
	}
	vars := map[string]struct{}{}
	for _, decl := range file.Decls {
		gen, ok := decl.(*ast.GenDecl)
		if !ok || gen.Tok != token.VAR {
			continue
		}
		for _, spec := range gen.Specs {
			vs, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}
			if !specEmbedsPath(rel, gen.Doc, vs.Doc, vs.Comment) {
				continue
			}
			for _, name := range vs.Names {
				vars[name.Name] = struct{}{}
			}
		}
	}
	return vars
}
F
function

specEmbedsPath

Parameters

rel
string
groups
...*ast.CommentGroup

Returns

bool
cmd/rfw/server/hmr_template.go:87-110
func specEmbedsPath(rel string, groups ...*ast.CommentGroup) bool

{
	for _, grp := range groups {
		if grp == nil {
			continue
		}
		for _, comment := range grp.List {
			text := strings.TrimSpace(comment.Text)
			if !strings.HasPrefix(text, "//go:embed") {
				continue
			}
			fields := strings.Fields(strings.TrimPrefix(text, "//go:embed"))
			for _, field := range fields {
				candidate := strings.Trim(field, "`\"")
				if candidate == "" {
					continue
				}
				if filepath.ToSlash(candidate) == rel {
					return true
				}
			}
		}
	}
	return false
}
F
function

componentsUsingTemplates

Parameters

goFile
string
vars
map[string]struct{}

Returns

[]string
cmd/rfw/server/hmr_template.go:112-161
func componentsUsingTemplates(goFile string, vars map[string]struct{}) []string

{
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, goFile, nil, 0)
	if err != nil {
		return nil
	}
	var names []string
	ast.Inspect(file, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok || len(call.Args) < 2 {
			return true
		}
		fun := call.Fun
		switch f := fun.(type) {
		case *ast.IndexExpr:
			fun = f.X
		case *ast.IndexListExpr:
			fun = f.X
		}
		sel, ok := fun.(*ast.SelectorExpr)
		if !ok {
			return true
		}
		pkg, ok := sel.X.(*ast.Ident)
		if !ok || pkg.Name != "core" {
			return true
		}
		if sel.Sel.Name != "NewComponent" && sel.Sel.Name != "NewComponentWith" {
			return true
		}
		lit, ok := call.Args[0].(*ast.BasicLit)
		if !ok || lit.Kind != token.STRING {
			return true
		}
		name, err := strconv.Unquote(lit.Value)
		if err != nil || name == "" {
			return true
		}
		ident, ok := call.Args[1].(*ast.Ident)
		if !ok {
			return true
		}
		if _, ok := vars[ident.Name]; !ok {
			return true
		}
		names = append(names, name)
		return true
	})
	return names
}
F
function

TestComponentNamesForTemplate

Parameters

cmd/rfw/server/hmr_template_test.go:10-27
func TestComponentNamesForTemplate(t *testing.T)

{
	tmpl := filepath.Join("testdata", "components", "templates", "input_component.rtml")
	names := componentNamesForTemplate(tmpl)
	t.Logf("names: %v", names)
	if len(names) == 0 {
		t.Fatalf("expected component names for %s", tmpl)
	}
	found := false
	for _, name := range names {
		if name == "InputComponent" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("expected InputComponent in %v", names)
	}
}
F
function

TestComponentNamesForTemplateGenerics

Parameters

cmd/rfw/server/hmr_template_test.go:29-43
func TestComponentNamesForTemplateGenerics(t *testing.T)

{
	tmpl := filepath.Join("testdata", "components", "templates", "webgl_component.rtml")
	names := componentNamesForTemplate(tmpl)
	t.Logf("names: %v", names)
	found := false
	for _, name := range names {
		if name == "WebGLComponent" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("expected WebGLComponent in %v", names)
	}
}
F
function

TestComponentNamesForTemplateNoMatch

Parameters

cmd/rfw/server/hmr_template_test.go:45-50
func TestComponentNamesForTemplateNoMatch(t *testing.T)

{
	tmpl := filepath.Join("hmr_template_test.go")
	if names := componentNamesForTemplate(tmpl); names != nil {
		t.Fatalf("expected nil, got %v", names)
	}
}
S
struct

Server

Server builds, serves, and watches an RFW project during development.

cmd/rfw/server/server.go:35-46
type Server struct

Methods

handleHMR
Method
func (*Server) handleHMR(w http.ResponseWriter, r *http.Request)
{
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "stream unsupported", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	client := make(chan []byte, 8)

	s.hmrMu.Lock()
	s.hmrClients[client] = struct{}{}
	s.hmrMu.Unlock()

	utils.Debug("hmr client connected")

	if _, err := fmt.Fprint(w, ": connected\n\n"); err != nil {
		return
	}
	flusher.Flush()

	defer func() {
		s.hmrMu.Lock()
		delete(s.hmrClients, client)
		s.hmrMu.Unlock()
		utils.Debug("hmr client disconnected")
	}()

	ping := time.NewTicker(30 * time.Second)
	defer ping.Stop()

	for {
		select {
		case msg, ok := <-client:
			if !ok {
				return
			}
			if _, err := fmt.Fprintf(w, "data: %s\n\n", msg); err != nil {
				return
			}
			flusher.Flush()
		case <-ping.C:
			if _, err := fmt.Fprint(w, ": ping\n\n"); err != nil {
				return
			}
			flusher.Flush()
		case <-r.Context().Done():
			return
		}
	}
}
broadcast
Method

Parameters

Returns

error
func (*Server) broadcast(msg devMessage) error
{
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}

	s.hmrMu.Lock()
	defer s.hmrMu.Unlock()
	if len(s.hmrClients) == 0 {
		return nil
	}
	for ch := range s.hmrClients {
		select {
		case ch <- data:
		default:
			// Drop slow clients to avoid blocking rebuilds.
			delete(s.hmrClients, ch)
			close(ch)
		}
	}
	return nil
}

Parameters

path string

Returns

error
func (*Server) broadcastReload(path string) error
{
	rel := path
	if abs, err := filepath.Abs(path); err == nil {
		if cwd, err := filepath.Abs("."); err == nil {
			if r, err := filepath.Rel(cwd, abs); err == nil {
				rel = filepath.ToSlash(r)
			}
		}
	}
	utils.Debug(fmt.Sprintf("broadcasting reload for %s", rel))
	return s.broadcast(devMessage{Type: "reload", Path: rel})
}

Parameters

path string
component string
markup string

Returns

error
func (*Server) broadcastTemplateUpdate(path, component, markup string) error
{
	rel := path
	if abs, err := filepath.Abs(path); err == nil {
		if cwd, err := filepath.Abs("."); err == nil {
			if r, err := filepath.Rel(cwd, abs); err == nil {
				rel = filepath.ToSlash(r)
			}
		}
	}
	utils.Debug(fmt.Sprintf("streaming template update for %s (%s)", rel, component))
	return s.broadcast(devMessage{Type: "rtml", Path: rel, Component: component, Markup: markup})
}
Start
Method

Start builds the project and starts its development servers and watcher.

Returns

error
func (*Server) Start() error
{
	if err := build.Build(); err != nil {
		return err
	}

	// Detect build type from manifest to know if host components are enabled.
	s.buildType = readBuildType()
	var mux *http.ServeMux
	httpsPort := incrementPort(s.Port)
	if s.buildType == "ssc" {
		s.hostPort = incrementPort(httpsPort)
		if err := s.startHost(); err != nil {
			return err
		}
		target := &url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%s", s.hostPort)}
		proxy := httputil.NewSingleHostReverseProxy(target)
		mux = http.NewServeMux()
		mux.HandleFunc("/__rfw/hmr", s.handleHMR)
		mux.Handle("/", proxy)
	} else {
		root := filepath.Join("build", "client")
		mux = hostpkg.NewMux(root)
		mux.HandleFunc("/__rfw/hmr", s.handleHMR)
	}
	server := &http.Server{
		Addr:              ":" + s.Port,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	go func() {
		if err := server.ListenAndServe(); err != nil {
			utils.Fatal("Server failed: ", err)
		}
	}()
	go func() {
		if err := hostpkg.ListenAndServeTLSWithMux(":"+httpsPort, mux); err != nil {
			utils.Fatal("HTTPS server failed: ", err)
		}
	}()
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return err
	}
	s.watcher = watcher
	if err := s.addWatchers("."); err != nil {
		return err
	}
	go s.watchFiles()

	signal.Notify(s.stopCh, syscall.SIGINT, syscall.SIGTERM)

	localIP := ""
	if s.Host {
		localIP, err = utils.GetLocalIP()
		if err != nil {
			return err
		}
	}

	utils.ClearScreen()
	utils.PrintStartupInfo(s.Port, httpsPort, localIP, s.Host)

	go s.listenForCommands()

	<-s.stopCh
	utils.Info("Server stopped.")
	s.stopHost()
	return nil
}
func (*Server) listenForCommands()
{
	reader := bufio.NewReader(os.Stdin)
	for {
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(input)

		switch strings.ToLower(input) {
		case "h":
			utils.PrintHelp()
		case "u":
			utils.ClearScreen()
			localIP, err := utils.GetLocalIP()
			if err != nil {
				utils.Fatal("Failed to get local IP address: ", err)
			}
			httpsPort := incrementPort(s.Port)
			utils.PrintStartupInfo(s.Port, httpsPort, localIP, s.Host)
		case "c", "q":
			utils.Info("Closing the server...")
			s.stopCh <- syscall.SIGINT
			return
		case "o":
			utils.Info("Opening the browser...")
			url := fmt.Sprintf("http://localhost:%s/", s.Port)
			if err := utils.OpenBrowser(url); err != nil {
				utils.Info(fmt.Sprintf("Failed to open browser: %v", err))
			}
		default:
			utils.Info("Unknown command. Press 'h' for help.")
		}
	}
}
addWatchers
Method

Parameters

root string

Returns

error
func (*Server) addWatchers(root string) error
{
	return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			name := d.Name()
			if name != "." && (name == "build" || strings.HasPrefix(name, ".")) {
				return filepath.SkipDir
			}
			return s.watcher.Add(path)
		}
		return nil
	})
}
shouldIgnore
Method

Parameters

now time.Time

Returns

bool
func (*Server) shouldIgnore(now time.Time) bool
{
	return now.Before(s.ignoreUntil)
}
watchFiles
Method
func (*Server) watchFiles()
{
	var (
		deb         debouncer
		pendingPath string
		pendingSwap bool
	)
	for {
		select {
		case event, ok := <-s.watcher.Events:
			if !ok {
				return
			}
			if s.shouldIgnore(time.Now()) {
				continue
			}
			utils.Debug(fmt.Sprintf("event: %s", event))
			if event.Op&fsnotify.Create != 0 {
				if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
					name := filepath.Base(event.Name)
					if name == "build" || strings.HasPrefix(name, ".") {
						continue
					}
					utils.Debug(fmt.Sprintf("watching new directory: %s", event.Name))
					_ = s.watcher.Add(event.Name)
					continue
				}
			}
			if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 {
				if isGenerated(event.Name) {
					continue
				}
				if strings.HasSuffix(event.Name, ".go") ||
					strings.HasSuffix(event.Name, ".rtml") ||
					strings.HasSuffix(event.Name, ".md") ||
					plugins.NeedsRebuild(event.Name) {
					// Template edits stream to the browser immediately: the
					// hot swap does not depend on the wasm rebuild, which
					// then runs to keep dist consistent for full reloads.
					if strings.HasSuffix(event.Name, ".rtml") {
						if markup, err := os.ReadFile(event.Name); err == nil {
							if comps := componentNamesForTemplate(event.Name); len(comps) > 0 {
								pendingSwap = true
								for _, name := range comps {
									if err := s.broadcastTemplateUpdate(event.Name, name, string(markup)); err != nil {
										utils.Debug(fmt.Sprintf("template broadcast failed: %v", err))
									}
								}
							}
						} else {
							utils.Debug(fmt.Sprintf("failed reading template %s: %v", event.Name, err))
						}
					}
					// Coalesce bursts of events into a single rebuild.
					pendingPath = event.Name
					deb.trigger(rebuildDebounce)
				}
			}
		case <-deb.C:
			deb.fired()
			s.rebuild(pendingPath, pendingSwap)
			pendingSwap = false
		case err, ok := <-s.watcher.Errors:
			if !ok {
				return
			}
			utils.Info(fmt.Sprintf("Watcher error: %v", err))
		case <-s.stopCh:
			if err := s.watcher.Close(); err != nil {
				utils.Debug(fmt.Sprintf("watcher close failed: %v", err))
			}
			s.stopHost()
			return
		}
	}
}
rebuild
Method

rebuild runs a full project build for a change at path, reporting timing and streaming the result to connected browsers.

Parameters

path string
templateSwapped bool
func (*Server) rebuild(path string, templateSwapped bool)
{
	rebuilds.Add(1)
	utils.Info("Changes detected, rebuilding...")
	start := time.Now()
	if err := build.Build(); err != nil {
		if emitErr := signalbus.Emit(context.Background(), RebuildBus, RebuildEvent{Path: path, Success: false, Error: err.Error()}); emitErr != nil {
			utils.Debug(fmt.Sprintf("rebuild event emit failed: %v", emitErr))
		}
		// A compile error must not kill the dev server: keep
		// watching so the next save can succeed.
		utils.Error("Failed to rebuild project: ", err)
		return
	}
	if emitErr := signalbus.Emit(context.Background(), RebuildBus, RebuildEvent{Path: path, Success: true}); emitErr != nil {
		utils.Debug(fmt.Sprintf("rebuild event emit failed: %v", emitErr))
	}
	utils.Info(fmt.Sprintf("Rebuilt in %s", time.Since(start).Round(time.Millisecond)))
	if !templateSwapped {
		if err := s.broadcastReload(path); err != nil {
			utils.Debug(fmt.Sprintf("hmr broadcast skipped: %v", err))
		}
	}
	if s.buildType == "ssc" {
		s.stopHost()
		if err := s.startHost(); err != nil {
			utils.Fatal("Failed to restart host server: ", err)
		}
	}
	s.ignoreUntil = time.Now().Add(ignoreDelay)
	drainWatcher(s.watcher)
}
startHost
Method

Returns

error
func (*Server) startHost() error
{
	path := filepath.Join("build", "host", "host")
	absPath, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	info, err := os.Lstat(absPath)
	if err != nil {
		return err
	}
	if !info.Mode().IsRegular() {
		return fmt.Errorf("host binary is not a regular file: %s", absPath)
	}
	s.hostCmd = exec.Command("./build/host/host")
	s.hostCmd.Stdout = os.Stdout
	s.hostCmd.Stderr = os.Stderr
	if s.hostPort != "" {
		env := os.Environ()
		env = append(env, fmt.Sprintf("RFW_HOST_PORT=%s", s.hostPort))
		s.hostCmd.Env = env
	}
	return s.hostCmd.Start()
}
stopHost
Method

stopHost asks the host process to exit with SIGTERM so it can close its sockets cleanly, then kills it if it has not exited within hostStopTimeout.

func (*Server) stopHost()
{
	if s.hostCmd == nil || s.hostCmd.Process == nil {
		s.hostCmd = nil
		return
	}
	proc := s.hostCmd.Process
	if err := proc.Signal(syscall.SIGTERM); err == nil {
		done := make(chan struct{})
		go func() {
			_, _ = proc.Wait()
			close(done)
		}()
		select {
		case <-done:
			s.hostCmd = nil
			return
		case <-time.After(hostStopTimeout):
			utils.Debug("host did not exit after SIGTERM, killing")
		}
	}
	_ = proc.Kill()
	_, _ = proc.Wait()
	s.hostCmd = nil
}

Fields

Name Type Description
Port string
Host bool
stopCh chan os.Signal
watcher *fsnotify.Watcher
hostCmd *exec.Cmd
buildType string
hostPort string
ignoreUntil time.Time
hmrMu sync.Mutex
hmrClients map[chan []byte]struct{}
S
struct

debouncer

debouncer coalesces bursts of triggers into a single fire on C after the
configured delay; every trigger during the window restarts the countdown.

cmd/rfw/server/server.go:60-63
type debouncer struct

Methods

trigger
Method

Parameters

func (*debouncer) trigger(delay time.Duration)
{
	if d.timer == nil {
		d.timer = time.NewTimer(delay)
		d.C = d.timer.C
		return
	}
	if !d.timer.Stop() {
		select {
		case <-d.timer.C:
		default:
		}
	}
	d.timer.Reset(delay)
}
fired
Method
func (*debouncer) fired()
{
	d.timer = nil
	d.C = nil
}

Fields

Name Type Description
timer *time.Timer
C <-chan time.Time
F
function

NewServer

NewServer creates a development server on port.

Parameters

port
string
host
bool

Returns

cmd/rfw/server/server.go:86-93
func NewServer(port string, host bool) *Server

{
	return &Server{
		Port:       port,
		Host:       host,
		stopCh:     make(chan os.Signal, 1),
		hmrClients: make(map[chan []byte]struct{}),
	}
}
F
function

isGenerated

Parameters

path
string

Returns

bool
cmd/rfw/server/server.go:215-217
func isGenerated(path string) bool

{
	return strings.HasPrefix(filepath.Base(path), "rfw_")
}
F
function

drainWatcher

Parameters

cmd/rfw/server/server.go:223-232
func drainWatcher(w *fsnotify.Watcher)

{
	for {
		select {
		case <-w.Events:
		case <-w.Errors:
		case <-time.After(50 * time.Millisecond):
			return
		}
	}
}
F
function

readBuildType

readBuildType reads the build type from rfw.json if present.

Returns

string
cmd/rfw/server/server.go:345-357
func readBuildType() string

{
	var manifest struct {
		Build struct {
			Type string `json:"type"`
		} `json:"build"`
	}
	data, err := os.ReadFile("rfw.json")
	if err != nil {
		return ""
	}
	_ = json.Unmarshal(data, &manifest)
	return strings.ToLower(manifest.Build.Type)
}
F
function

incrementPort

Parameters

port
string

Returns

string
cmd/rfw/server/server.go:359-362
func incrementPort(port string) string

{
	p, _ := strconv.Atoi(port)
	return strconv.Itoa(p + 1)
}