package main import ( "flag" "fmt" "net/http" "os" "os/exec" "os/user" "path/filepath" "regexp" "strings" "github.com/pelletier/go-toml/v2" lua "github.com/yuin/gopher-lua" ) const version = "1.0.0" type MyConfig struct { Site struct { Name string `toml:"name"` Version string `toml:"version"` Authors []string `toml:"authors"` } `toml:"site"` Fes struct { Version string `toml:"version"` CUSTOM_CSS string `toml:"CUSTOM_CSS,omitempty"` } `toml:"fes"` } var port = flag.Int("p", 3000, "set the server port") func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) { L := lua.NewState() defer L.Close() L.PreloadModule("fes", func(L *lua.LState) int { mod := L.NewTable() wd, _ := os.Getwd() corePath := filepath.Join(wd, "core") files, _ := os.ReadDir(corePath) for _, f := range files { if f.IsDir() || filepath.Ext(f.Name()) != ".lua" { continue } modName := f.Name()[:len(f.Name())-len(".lua")] if err := L.DoFile(filepath.Join(corePath, f.Name())); err != nil { fmt.Println("error loading", f.Name(), ":", err) continue } val := L.Get(-1) L.Pop(1) tbl, ok := val.(*lua.LTable) if !ok { t := L.NewTable() t.RawSetString("value", val) tbl = t } if modName == "builtin" { tbl.ForEach(func(key, value lua.LValue) { mod.RawSet(key, value) }) } else { mod.RawSetString(modName, tbl) } } // Pass config to Lua if cfg != nil { configTable := L.NewTable() siteTable := L.NewTable() siteTable.RawSetString("version", lua.LString(cfg.Site.Version)) siteTable.RawSetString("name", lua.LString(cfg.Site.Name)) authorsTable := L.NewTable() for i, author := range cfg.Site.Authors { authorsTable.RawSetInt(i+1, lua.LString(author)) } siteTable.RawSetString("authors", authorsTable) configTable.RawSetString("site", siteTable) fesTable := L.NewTable() fesTable.RawSetString("version", lua.LString(cfg.Fes.Version)) configTable.RawSetString("fes", fesTable) mod.RawSetString("config", configTable) } L.Push(mod) return 1 }) if err := L.DoFile(entry); err != nil { return "", err } top := L.GetTop() if top == 0 { fmt.Println("warning: no return value from Lua file") return "", nil } resultVal := L.Get(-1) L.SetGlobal("__fes_result", resultVal) if err := L.DoString("return tostring(__fes_result)"); err != nil { L.GetGlobal("__fes_result") if s := L.ToString(-1); s != "" { return s, nil } return "", nil } if s := L.ToString(-1); s != "" { return s, nil } return "", nil } func getName() string { out, err := exec.Command("git", "config", "user.name").Output() if err == nil { s := strings.TrimSpace(string(out)) if s != "" { return s } } u, err := user.Current() if err == nil && u.Username != "" { return u.Username } return "" } func newProject(dir string) error { if err := os.MkdirAll(filepath.Join(dir, "www"), 0755); err != nil { return err } indexLua := filepath.Join(dir, "www", "index.lua") if _, err := os.Stat(indexLua); os.IsNotExist(err) { content := `local fes = require("fes") local site = fes.site_builder() site:h1("Hello, World!") return site ` if err := os.WriteFile(indexLua, []byte(content), 0644); err != nil { return err } } indexFes := filepath.Join(dir, "Fes.toml") if _, err := os.Stat(indexFes); os.IsNotExist(err) { content := fmt.Sprintf(`[site] name = "%s" version = "0.0.1" authors = ["%s"] [fes] version = "%s" CUSTOM_CSS = `, dir, getName(), version) if err := os.WriteFile(indexFes, []byte(content), 0644); err != nil { return err } } fmt.Println("Created new project at", dir) return nil } func fixMalformedToml(content string) string { // Fix lines like "CUSTOM_CSS =" (with no value) to "CUSTOM_CSS = \"\"" re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`) return re.ReplaceAllStringFunc(content, func(match string) string { // Extract the key name parts := strings.Split(strings.TrimSpace(match), "=") if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" { key := strings.TrimSpace(parts[0]) return key + " = \"\"" } return match }) } func startServer(dir string) error { doc, err := os.ReadFile(filepath.Join(dir, "Fes.toml")) if err != nil { return err } // Fix malformed TOML before parsing docStr := fixMalformedToml(string(doc)) var cfg MyConfig err = toml.Unmarshal([]byte(docStr), &cfg) if err != nil { return fmt.Errorf("failed to parse Fes.toml: %w", err) } luaPath := filepath.Join(dir, "www", "index.lua") data, err := loadLua(dir, luaPath, &cfg) if err != nil { return err } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(data)) }) fmt.Printf("App running at:\n - Local: http://localhost:%d/\n", *port) return http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) } func main() { flag.Parse() if len(os.Args) < 3 { fmt.Println("Usage: fes ") fmt.Println("Commands: new, serve") os.Exit(1) } cmd := os.Args[1] dir := os.Args[2] switch cmd { case "new": if err := newProject(dir); err != nil { panic(err) } case "run": if err := startServer(dir); err != nil { panic(err) } default: fmt.Println("Unknown command:", cmd) os.Exit(1) } }