diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 06cc7353..fc9debc1 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -706,7 +706,7 @@ func (api *APIBuilder) Use(handlers ...context.Handler) { // It doesn't care about call order, it will prepend the handlers to all // existing routes and the future routes that may being registered. // -// The difference from `.DoneGLobal` is that this/or these Handler(s) are being always running first. +// The difference from `.DoneGlobal` is that this/or these Handler(s) are being always running first. // Use of `ctx.Next()` of those handler(s) is necessary to call the main handler or the next middleware. // It's always a good practise to call it right before the `Application#Run` function. func (api *APIBuilder) UseGlobal(handlers ...context.Handler) { diff --git a/iris.go b/iris.go index 41c923ae..3cb71ee7 100644 --- a/iris.go +++ b/iris.go @@ -2,7 +2,7 @@ package iris import ( // std packages - + "bytes" stdContext "context" "errors" "fmt" @@ -11,12 +11,11 @@ import ( "net" "net/http" "os" + "path/filepath" "strings" "sync" "time" - "github.com/kataras/golog" - // context for the handlers "github.com/kataras/iris/v12/context" // core packages, required to build the application @@ -36,6 +35,9 @@ import ( // handlers used in `Default` function requestLogger "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover" + + "github.com/kataras/golog" + "gopkg.in/yaml.v3" ) // Version is the current version number of the Iris Web Framework. @@ -875,6 +877,11 @@ func (app *Application) Build() error { if !app.Router.Downgraded() { // router + + if err := app.tryInjectLiveReload(); err != nil { + rp.Errf("LiveReload: init: failed: %v", err) + } + // create the request handler, the default routing handler routerHandler := router.NewDefaultHandler() err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false) @@ -943,6 +950,106 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error { return err } +// tryInjectLiveReload tries to check if this application +// runs under https://github.com/kataras/iris-cli and if so +// then it checks if the livereload is enabled and then injects +// the watch listener (js script) on every HTML response. +// It has a slight performance cost but +// this (iris-cli with watch and livereload enabled) +// is meant to be used only in development mode. +// It does a full reload at the moment and if the port changed +// at runtime it will fire 404 instead of redirecting to the correct port (that's a TODO). +// +// tryInjectLiveReload runs right before Build -> BuildRouter. +func (app *Application) tryInjectLiveReload() error { + conf := struct { + Running bool `yaml:"Running,omitempty"` + LiveReload struct { + Disable bool `yaml:"Disable"` + Port int `yaml:"Port"` + } `yaml:"LiveReload"` + }{} + // defaults to disabled here. + conf.LiveReload.Disable = true + + wd, err := os.Getwd() + if err != nil { + return err + } + + for _, path := range []string{".iris.yml" /*, "../.iris.yml", "../../.iris.yml" */} { + path = filepath.Join(wd, path) + + if _, err := os.Stat(path); err == nil { + inFile, err := os.OpenFile(path, os.O_RDONLY, 0644) + if err != nil { + return err + } + + dec := yaml.NewDecoder(inFile) + err = dec.Decode(&conf) + inFile.Close() + if err != nil { + return err + } + + break + } + } + + if !conf.Running || conf.LiveReload.Disable { + return nil + } + + scriptReloadJS := []byte(fmt.Sprintf(``, conf.LiveReload.Port)) + + bodyCloseTag := []byte("") + + app.WrapRouter(func(w http.ResponseWriter, r *http.Request, _ http.HandlerFunc) { + ctx := app.ContextPool.Acquire(w, r) + rec := ctx.Recorder() // Record everything and write all in once at the Context release. + app.ServeHTTPC(ctx) // We directly call request handler with Context. + + if strings.HasPrefix(ctx.GetContentType(), "text/html") { + // delete(rec.Header(), context.ContentLengthHeaderKey) + + body := rec.Body() + + if idx := bytes.LastIndex(body, bodyCloseTag); idx > 0 { + // add the script right before last . + body = append(body[:idx], bytes.Replace(body[idx:], bodyCloseTag, append(scriptReloadJS, bodyCloseTag...), 1)...) + rec.SetBody(body) + } else { + // Just append it. + rec.Write(scriptReloadJS) + } + + if _, has := rec.Header()[context.ContentLengthHeaderKey]; has { + rec.Header().Set(context.ContentLengthHeaderKey, fmt.Sprintf("%d", len(rec.Body()))) + } + } + + app.ContextPool.Release(ctx) + }) + + return nil +} + // https://ngrok.com/docs func (app *Application) tryStartTunneling() { if !app.config.Tunneling.isEnabled() {