diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml new file mode 100644 index 0000000..547dec7 --- /dev/null +++ b/.github/workflows/run-cli-tests.yml @@ -0,0 +1,33 @@ +name: CLI Tests + +on: + push: + branches: + - master + pull_request: + branches: + - '**' # Runs on any pull request to any branch + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' # Specify the Go version you need + + - name: Install dependencies + run: cd ./cli/htmgo && go mod download + + - name: Run Go tests + run: cd ./cli/htmgo && go test ./... -coverprofile=coverage.txt + + - name: Upload results to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/run-framework-tests.yml b/.github/workflows/run-framework-tests.yml index 87cce04..a342dee 100644 --- a/.github/workflows/run-framework-tests.yml +++ b/.github/workflows/run-framework-tests.yml @@ -3,7 +3,7 @@ name: Framework Tests on: push: branches: - - '**' # Runs on any branch push + - master pull_request: branches: - '**' # Runs on any pull request to any branch diff --git a/cli/htmgo/codecov.yml b/cli/htmgo/codecov.yml new file mode 100644 index 0000000..73f5f03 --- /dev/null +++ b/cli/htmgo/codecov.yml @@ -0,0 +1,5 @@ +coverage: + paths: + - "tasks/astgen/**" + ignore: + - "**" diff --git a/cli/htmgo/go.mod b/cli/htmgo/go.mod index 07945a3..7d67f4e 100644 --- a/cli/htmgo/go.mod +++ b/cli/htmgo/go.mod @@ -7,11 +7,17 @@ require ( github.com/google/uuid v1.6.0 github.com/maddalax/htmgo/framework v1.0.3-0.20241109183230-b234ead96499 github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20241109183230-b234ead96499 + github.com/stretchr/testify v1.9.0 golang.org/x/mod v0.21.0 golang.org/x/sys v0.26.0 golang.org/x/tools v0.25.0 ) +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) + require ( github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/go-chi/chi/v5 v5.1.0 // indirect diff --git a/cli/htmgo/tasks/astgen/entry.go b/cli/htmgo/tasks/astgen/entry.go index d53dc85..fc86b59 100644 --- a/cli/htmgo/tasks/astgen/entry.go +++ b/cli/htmgo/tasks/astgen/entry.go @@ -233,59 +233,34 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) { } func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) { - fName := "GetPartialFromContext" - - body := ` - path := r.URL.Path - ` - - if len(partials) == 0 { - body = "" - } - moduleName := GetModuleName() - for _, f := range partials { - if f.FuncName == fName { - continue - } - caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName) - path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName) - body += fmt.Sprintf(` - if path == "%s" || path == "%s" { - cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext) - return %s(cc) - } - `, f.FuncName, path, caller) - } - - body += "return nil" - - f := Function{ - Name: fName, - Parameters: []NameType{ - {Name: "r", Type: "*http.Request"}, - }, - Return: []ReturnType{ - {Type: "*h.Partial"}, - }, - Body: body, - } - - builder.Append(builder.BuildFunction(f)) - - registerFunction := fmt.Sprintf(` - func RegisterPartials(router *chi.Mux) { - router.Handle("/%s/partials*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - partial := GetPartialFromContext(r) + var routerHandlerMethod = func(path string, caller string) string { + return fmt.Sprintf(` + router.Handle("%s", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext) + partial := %s(cc) if partial == nil { w.WriteHeader(404) return } h.PartialView(w, partial) - })) + }))`, path, caller) + } + + handlerMethods := make([]string, 0) + + for _, f := range partials { + caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName) + path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName) + handlerMethods = append(handlerMethods, routerHandlerMethod(path, caller)) + } + + registerFunction := fmt.Sprintf(` + func RegisterPartials(router *chi.Mux) { + %s } - `, moduleName) + `, strings.Join(handlerMethods, "\n")) builder.AppendLine(registerFunction) } @@ -294,7 +269,7 @@ func writePartialsFile() { config := dirutil.GetConfig() cwd := process.GetWorkingDir() - partialPath := filepath.Join(cwd, "partials") + partialPath := filepath.Join(cwd) partials, err := findPublicFuncsReturningHPartial(partialPath, func(partial Partial) bool { return partial.FuncName != "GetPartialFromContext" }) @@ -311,10 +286,13 @@ func writePartialsFile() { builder := NewCodeBuilder(nil) builder.AppendLine(GeneratedFileLine) builder.AppendLine(PackageName) - builder.AddImport(ModuleName) - builder.AddImport(HttpModuleName) builder.AddImport(ChiModuleName) + if len(partials) > 0 { + builder.AddImport(ModuleName) + builder.AddImport(HttpModuleName) + } + moduleName := GetModuleName() for _, partial := range partials { builder.AddImport(fmt.Sprintf(`%s/%s`, moduleName, partial.Import)) diff --git a/cli/htmgo/tasks/astgen/project-sample/.gitignore b/cli/htmgo/tasks/astgen/project-sample/.gitignore new file mode 100644 index 0000000..3d6a979 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/.gitignore @@ -0,0 +1,6 @@ +/assets/dist +tmp +node_modules +.idea +__htmgo +dist \ No newline at end of file diff --git a/cli/htmgo/tasks/astgen/project-sample/assets.go b/cli/htmgo/tasks/astgen/project-sample/assets.go new file mode 100644 index 0000000..0a7e4a2 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/assets.go @@ -0,0 +1,13 @@ +//go:build !prod +// +build !prod + +package main + +import ( + "astgen-project-sample/internal/embedded" + "io/fs" +) + +func GetStaticAssets() fs.FS { + return embedded.NewOsFs() +} diff --git a/cli/htmgo/tasks/astgen/project-sample/assets_prod.go b/cli/htmgo/tasks/astgen/project-sample/assets_prod.go new file mode 100644 index 0000000..f0598e1 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/assets_prod.go @@ -0,0 +1,16 @@ +//go:build prod +// +build prod + +package main + +import ( + "embed" + "io/fs" +) + +//go:embed assets/dist/* +var staticAssets embed.FS + +func GetStaticAssets() fs.FS { + return staticAssets +} diff --git a/cli/htmgo/tasks/astgen/project-sample/go.mod b/cli/htmgo/tasks/astgen/project-sample/go.mod new file mode 100644 index 0000000..981c812 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/go.mod @@ -0,0 +1,13 @@ +module astgen-project-sample + +go 1.23.0 + +require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/maddalax/htmgo/framework v1.0.3-0.20241109183230-b234ead96499 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cli/htmgo/tasks/astgen/project-sample/go.sum b/cli/htmgo/tasks/astgen/project-sample/go.sum new file mode 100644 index 0000000..bbed914 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/maddalax/htmgo/framework v1.0.3-0.20241109183230-b234ead96499 h1:hQF++Rt9lJHUFk8PG097MTbpl2NnEcbot0iy8ZVPSOI= +github.com/maddalax/htmgo/framework v1.0.3-0.20241109183230-b234ead96499/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/htmgo/tasks/astgen/project-sample/htmgo.yml b/cli/htmgo/tasks/astgen/project-sample/htmgo.yml new file mode 100644 index 0000000..3074bbe --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/htmgo.yml @@ -0,0 +1,21 @@ +# htmgo configuration + +# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist +tailwind: true + +# which directories to ignore when watching for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_ignore: [".git", "node_modules", "dist/*"] + +# files to watch for changes, supports glob patterns through https://github.com/bmatcuk/doublestar +watch_files: ["**/*.go", "**/*.css", "**/*.md"] + +# files or directories to ignore when automatically registering routes for pages +# supports glob patterns through https://github.com/bmatcuk/doublestar +automatic_page_routing_ignore: ["root.go"] + +# files or directories to ignore when automatically registering routes for partials +# supports glob patterns through https://github.com/bmatcuk/doublestar +automatic_partial_routing_ignore: [] + +# url path of where the public assets are located +public_asset_path: "/public" diff --git a/cli/htmgo/tasks/astgen/project-sample/internal/embedded/os.go b/cli/htmgo/tasks/astgen/project-sample/internal/embedded/os.go new file mode 100644 index 0000000..ddfd55f --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/internal/embedded/os.go @@ -0,0 +1,17 @@ +package embedded + +import ( + "io/fs" + "os" +) + +type OsFs struct { +} + +func (receiver OsFs) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func NewOsFs() OsFs { + return OsFs{} +} diff --git a/cli/htmgo/tasks/astgen/project-sample/main.go b/cli/htmgo/tasks/astgen/project-sample/main.go new file mode 100644 index 0000000..1d57a6b --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "astgen-project-sample/__htmgo" + "fmt" + "github.com/maddalax/htmgo/framework/config" + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/service" + "io/fs" + "net/http" +) + +func main() { + locator := service.NewLocator() + cfg := config.Get() + + h.Start(h.AppOpts{ + ServiceLocator: locator, + LiveReload: true, + Register: func(app *h.App) { + sub, err := fs.Sub(GetStaticAssets(), "assets/dist") + + if err != nil { + panic(err) + } + + http.FileServerFS(sub) + + // change this in htmgo.yml (public_asset_path) + app.Router.Handle(fmt.Sprintf("%s/*", cfg.PublicAssetPath), + http.StripPrefix(cfg.PublicAssetPath, http.FileServerFS(sub))) + + __htmgo.Register(app.Router) + }, + }) +} diff --git a/cli/htmgo/tasks/astgen/project-sample/pages/index.go b/cli/htmgo/tasks/astgen/project-sample/pages/index.go new file mode 100644 index 0000000..2eb3d58 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/pages/index.go @@ -0,0 +1,30 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" +) + +func IndexPage(ctx *h.RequestContext) *h.Page { + return RootPage( + h.Div( + h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"), + h.H3( + h.Id("intro-text"), + h.Text("hello htmgo"), + h.Class("text-5xl"), + ), + h.Div( + h.Class("mt-3"), + ), + h.Div(), + ), + ) +} + +func TestPartial(ctx *h.RequestContext) *h.Partial { + return h.NewPartial( + h.Div( + h.Text("Hello World"), + ), + ) +} diff --git a/cli/htmgo/tasks/astgen/project-sample/pages/root.go b/cli/htmgo/tasks/astgen/project-sample/pages/root.go new file mode 100644 index 0000000..65cb7a9 --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/pages/root.go @@ -0,0 +1,40 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" +) + +func RootPage(children ...h.Ren) *h.Page { + title := "htmgo template" + description := "an example of the htmgo template" + author := "htmgo" + url := "https://htmgo.dev" + + return h.NewPage( + h.Html( + h.HxExtensions( + h.BaseExtensions(), + ), + h.Head( + h.Title( + h.Text(title), + ), + h.Meta("viewport", "width=device-width, initial-scale=1"), + h.Meta("title", title), + h.Meta("charset", "utf-8"), + h.Meta("author", author), + h.Meta("description", description), + h.Meta("og:title", title), + h.Meta("og:url", url), + h.Link("canonical", url), + h.Meta("og:description", description), + ), + h.Body( + h.Div( + h.Class("flex flex-col gap-2 bg-white h-full"), + h.Fragment(children...), + ), + ), + ), + ) +} diff --git a/cli/htmgo/tasks/astgen/project-sample/partials/index.go b/cli/htmgo/tasks/astgen/project-sample/partials/index.go new file mode 100644 index 0000000..43f1aeb --- /dev/null +++ b/cli/htmgo/tasks/astgen/project-sample/partials/index.go @@ -0,0 +1,11 @@ +package partials + +import "github.com/maddalax/htmgo/framework/h" + +func CountersPartial(ctx *h.RequestContext) *h.Partial { + return h.NewPartial( + h.Div( + h.Text("my counter"), + ), + ) +} diff --git a/cli/htmgo/tasks/astgen/registration_test.go b/cli/htmgo/tasks/astgen/registration_test.go new file mode 100644 index 0000000..2e0b088 --- /dev/null +++ b/cli/htmgo/tasks/astgen/registration_test.go @@ -0,0 +1,66 @@ +package astgen + +import ( + "fmt" + "github.com/maddalax/htmgo/cli/htmgo/internal/dirutil" + "github.com/maddalax/htmgo/cli/htmgo/tasks/process" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestAstGen(t *testing.T) { + t.Parallel() + + workingDir, err := filepath.Abs("./project-sample") + + assert.NoError(t, err) + process.SetWorkingDir(workingDir) + assert.NoError(t, os.Chdir(workingDir)) + + err = dirutil.DeleteDir(filepath.Join(process.GetWorkingDir(), "__htmgo")) + assert.NoError(t, err) + err = process.Run(process.NewRawCommand("", "go build .")) + assert.Error(t, err) + err = GenAst() + assert.NoError(t, err) + + go func() { + // project was buildable after astgen, confirmed working + err = process.Run(process.NewRawCommand("server", "go run .")) + assert.NoError(t, err) + }() + + time.Sleep(time.Second * 1) + + urls := []string{ + "/astgen-project-sample/partials.CountersPartial", + "/", + "/astgen-project-sample/pages.TestPartial", + } + + defer func() { + serverProcess := process.GetProcessByName("server") + assert.NotNil(t, serverProcess) + process.KillProcess(*serverProcess) + }() + + wg := sync.WaitGroup{} + + for _, url := range urls { + wg.Add(1) + go func() { + defer wg.Done() + // ensure we can get a 200 response on the partials + resp, e := http.Get(fmt.Sprintf("http://localhost:3000%s", url)) + assert.NoError(t, e) + assert.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("%s was not a 200 response", url)) + }() + } + + wg.Wait() +} diff --git a/cli/htmgo/watcher.go b/cli/htmgo/watcher.go index 4838d14..6ec55c4 100644 --- a/cli/htmgo/watcher.go +++ b/cli/htmgo/watcher.go @@ -89,7 +89,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { if !ok { return } - slog.Error("error:", err.Error()) + slog.Error("error:", slog.String("error", err.Error())) } } }() @@ -118,7 +118,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) { if info.IsDir() { err = watcher.Add(path) if err != nil { - slog.Error("Error adding directory to watcher:", err) + slog.Error("Error adding directory to watcher:", slog.String("error", err.Error())) } else { slog.Debug("Watching directory:", slog.String("path", path)) }