diff --git a/go.mod b/go.mod index c28e641a..b4e30463 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/evanw/esbuild v0.21.4 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 4445ca15..08feef01 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= +github.com/evanw/esbuild v0.21.4 h1:pe4SEQMoR1maEjhgWPEPWmUy11Jp6nidxd1mOvMrFFU= +github.com/evanw/esbuild v0.21.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -369,6 +371,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 2377cbc8..4252b756 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( _ "git.handmade.network/hmn/hmn/src/admintools" _ "git.handmade.network/hmn/hmn/src/assets" - _ "git.handmade.network/hmn/hmn/src/buildscss" + _ "git.handmade.network/hmn/hmn/src/buildscss/cmd" _ "git.handmade.network/hmn/hmn/src/discord/cmd" _ "git.handmade.network/hmn/hmn/src/initimage" _ "git.handmade.network/hmn/hmn/src/migration" diff --git a/public/icons-7ANC2ICW.ttf b/public/icons-7ANC2ICW.ttf deleted file mode 100644 index 99dfe748..00000000 Binary files a/public/icons-7ANC2ICW.ttf and /dev/null differ diff --git a/src/rawdata/scss/icons.ttf b/public/icons.ttf similarity index 100% rename from src/rawdata/scss/icons.ttf rename to public/icons.ttf diff --git a/public/style.css b/public/style.css index 36ab2946..2e5603f6 100644 --- a/public/style.css +++ b/public/style.css @@ -8480,7 +8480,7 @@ header.clicked .root-item:not(.clicked) > .submenu { /* src/rawdata/scss/icons.css */ @font-face { font-family: icons; - src: url("./icons-7ANC2ICW.ttf?v=4"); + src: url(/public/icons.ttf?v=4); } span.icon { font-family: "icons"; diff --git a/src/buildscss/buildscss.go b/src/buildscss/buildscss.go index 006f1063..fdae7bae 100644 --- a/src/buildscss/buildscss.go +++ b/src/buildscss/buildscss.go @@ -1,85 +1,64 @@ package buildscss -/* +import ( + "context" -var compressed bool + "git.handmade.network/hmn/hmn/src/config" + "git.handmade.network/hmn/hmn/src/jobs" + "git.handmade.network/hmn/hmn/src/logging" + "github.com/evanw/esbuild/pkg/api" +) -func init() { - libsass.RegisterSassFunc("base64($filename)", func(ctx context.Context, in libsass.SassValue) (*libsass.SassValue, error) { - var filename string - err := libsass.Unmarshal(in, &filename) - if err != nil { - return nil, err - } +var ActiveServerPort uint16 - fileBytes, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - - encoded, _ := libsass.Marshal(base64.StdEncoding.EncodeToString(fileBytes)) - return &encoded, nil - }) - - buildCommand := &cobra.Command{ - Use: "buildscss", - Short: "Build the website CSS", - Run: func(cmd *cobra.Command, args []string) { - style := libsass.NESTED_STYLE - if compressed { - style = libsass.COMPRESSED_STYLE - } - - err := compile("src/rawdata/scss/style.scss", "public/style.css", "light", style) - if err != nil { - fmt.Println(color.Bold + color.Red + "Failed to compile main SCSS." + color.Reset) - fmt.Println(err) - os.Exit(1) - } - - for _, theme := range []string{"light", "dark"} { - err := compile("src/rawdata/scss/theme.scss", fmt.Sprintf("public/themes/%s/theme.css", theme), theme, style) - if err != nil { - fmt.Println(color.Bold + color.Red + "Failed to compile theme SCSS." + color.Reset) - fmt.Println(err) - os.Exit(1) - } - } +func RunServer(ctx context.Context) jobs.Job { + job := jobs.New() + if config.Config.Env != config.Dev { + job.Done() + return job + } + logger := logging.ExtractLogger(ctx).With().Str("module", "EsBuild").Logger() + esCtx, ctxErr := BuildContext() + if ctxErr != nil { + panic(ctxErr) + } + logger.Info().Msg("Starting esbuild server and watcher") + err := esCtx.Watch(api.WatchOptions{}) + serverResult, err := esCtx.Serve(api.ServeOptions{ + Port: config.Config.EsBuild.Port, + Servedir: "./", + OnRequest: func(args api.ServeOnRequestArgs) { + logger.Info().Interface("args", args).Msg("Response from esbuild server") }, + }) + if err != nil { + panic(err) } - buildCommand.Flags().BoolVar(&compressed, "compressed", false, "Minify the output CSS") + ActiveServerPort = serverResult.Port + go func() { + <-ctx.Done() + logger.Info().Msg("Shutting down esbuild server and watcher") + esCtx.Dispose() + job.Done() + }() - website.WebsiteCommand.AddCommand(buildCommand) + return job } -func compile(inpath, outpath string, theme string, style int) error { - err := os.MkdirAll(filepath.Dir(outpath), 0755) - if err != nil { - panic(oops.New(err, "failed to create directory for CSS file")) - } - - outfile, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - panic(oops.New(err, "failed to open CSS file for writing")) - } - defer outfile.Close() - - infile, err := os.Open(inpath) - if err != nil { - panic(oops.New(err, "failed to open SCSS file")) - } - compiler, err := libsass.New(outfile, infile, - libsass.IncludePaths([]string{ - "src/rawdata/scss", - fmt.Sprintf("src/rawdata/scss/themes/%s", theme), - }), - libsass.OutputStyle(style), - ) - if err != nil { - panic(oops.New(err, "failed to create SCSS compiler")) - } - - return compiler.Run() +func BuildContext() (api.BuildContext, *api.ContextError) { + return api.Context(api.BuildOptions{ + EntryPoints: []string{ + "src/rawdata/scss/style.css", + }, + Outbase: "src/rawdata/scss", + Outdir: "public", + External: []string{"/public/*"}, + Bundle: true, + Write: true, + Engines: []api.Engine{ + {Name: api.EngineChrome, Version: "109"}, + {Name: api.EngineFirefox, Version: "109"}, + {Name: api.EngineSafari, Version: "12"}, + }, + }) } - -*/ diff --git a/src/buildscss/cmd/cmd.go b/src/buildscss/cmd/cmd.go new file mode 100644 index 00000000..93e250d0 --- /dev/null +++ b/src/buildscss/cmd/cmd.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + "os" + + "git.handmade.network/hmn/hmn/src/buildscss" + "git.handmade.network/hmn/hmn/src/logging" + "git.handmade.network/hmn/hmn/src/website" + "github.com/spf13/cobra" +) + +func init() { + buildCommand := &cobra.Command{ + Use: "buildscss", + Short: "Build the website CSS", + Run: func(cmd *cobra.Command, args []string) { + ctx, err := buildscss.BuildContext() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + res := ctx.Rebuild() + outputFilenames := make([]string, 0) + for _, o := range res.OutputFiles { + outputFilenames = append(outputFilenames, o.Path) + } + logging.Info(). + Interface("Errors", res.Errors). + Interface("Warnings", res.Warnings). + Msg("Ran esbuild") + if len(outputFilenames) > 0 { + logging.Info().Interface("Files", outputFilenames).Msg("Wrote files") + } + }, + } + website.WebsiteCommand.AddCommand(buildCommand) +} diff --git a/src/config/config.go.example b/src/config/config.go.example index ef402e34..ac25c0d2 100644 --- a/src/config/config.go.example +++ b/src/config/config.go.example @@ -91,4 +91,7 @@ var Config = HMNConfig{ FFMpegPath: "", // Will not generate asset video thumbnails if ffmpeg is not specified CPULimitPath: "", // Not mandatory. FFMpeg will not limited if this is not provided }, + EsBuild: EsBuildConfig{ + Port: 9004, + }, } diff --git a/src/config/types.go b/src/config/types.go index f43baf20..c983ac05 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -32,7 +32,8 @@ type HMNConfig struct { EpisodeGuide EpisodeGuide DevConfig DevConfig PreviewGeneration PreviewGenerationConfig - Calendars []CalendarSource + Calendars []CalendarSource + EsBuild EsBuildConfig } type PostgresConfig struct { @@ -104,7 +105,7 @@ type MatrixConfig struct { type CalendarSource struct { Name string - Url string + Url string } type EpisodeGuide struct { @@ -126,6 +127,10 @@ type PreviewGenerationConfig struct { CPULimitPath string } +type EsBuildConfig struct { + Port uint16 +} + func init() { if Config.EpisodeGuide.Projects == nil { Config.EpisodeGuide.Projects = make(map[string]string) diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index a1dbb95b..eba526ea 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -981,6 +981,13 @@ func BuildS3Asset(s3key string) string { return res } +var RegexEsBuild = regexp.MustCompile("^/esbuild$") + +func BuildEsBuild() string { + defer CatchPanic() + return Url("/esbuild", nil) +} + var RegexPublic = regexp.MustCompile("^/public/.+$") func BuildPublic(filepath string, cachebust bool) string { diff --git a/src/rawdata/scss/icons.css b/src/rawdata/scss/icons.css index b7d5df99..4a6209a2 100644 --- a/src/rawdata/scss/icons.css +++ b/src/rawdata/scss/icons.css @@ -1,6 +1,6 @@ @font-face { font-family: icons; - src: url("icons.ttf?v=4"); + src: url("/public/icons.ttf?v=4"); } span.icon { @@ -70,4 +70,4 @@ span.icon-hitbox::before { span.icon-rss::before { font-family: "icons"; content: "4"; -} \ No newline at end of file +} diff --git a/src/rawdata/scss/vars.css b/src/rawdata/scss/vars.css index ccc0cf06..346f6736 100644 --- a/src/rawdata/scss/vars.css +++ b/src/rawdata/scss/vars.css @@ -102,4 +102,4 @@ $breakpoint-large: screen and (min-width: 60em) --spoiler-border: #777; } -} \ No newline at end of file +} diff --git a/src/templates/src/layouts/base.html b/src/templates/src/layouts/base.html index 6c3307f3..1e492e38 100644 --- a/src/templates/src/layouts/base.html +++ b/src/templates/src/layouts/base.html @@ -68,6 +68,29 @@ + {{ if .EsBuildSSEUrl }} + + {{ end }} + {{ block "extrahead" . }}{{ end }} diff --git a/src/templates/types.go b/src/templates/types.go index 0c2fc1ae..badf4542 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -23,6 +23,8 @@ type BaseData struct { DiscordInviteUrl string NewsletterSignupUrl string + EsBuildSSEUrl string + Project Project User *User Session *Session diff --git a/src/website/base_data.go b/src/website/base_data.go index 945a90a6..c3f13168 100644 --- a/src/website/base_data.go +++ b/src/website/base_data.go @@ -1,6 +1,7 @@ package website import ( + "git.handmade.network/hmn/hmn/src/buildscss" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/models" @@ -96,6 +97,10 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc }, } + if buildscss.ActiveServerPort != 0 { + baseData.EsBuildSSEUrl = hmnurl.BuildEsBuild() + } + if c.CurrentUser != nil { baseData.Header.UserProfileUrl = hmnurl.BuildUserProfile(c.CurrentUser.Username) } diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index d0b6680c..02a74820 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -408,6 +408,8 @@ type ResponseData struct { FutureNotices []templates.Notice header http.Header + + hijacked bool } var _ http.ResponseWriter = &ResponseData{} @@ -480,6 +482,12 @@ func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) { // Run the chosen handler res := h(c) + if res.hijacked { + // NOTE(asaf): In case we forward the request/response to another handler + // (like esbuild). + return + } + if res.StatusCode == 0 { res.StatusCode = http.StatusOK } diff --git a/src/website/routes.go b/src/website/routes.go index dc23a15c..f87bab6e 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -1,16 +1,22 @@ package website import ( + "bytes" "errors" "fmt" + "io" "net/http" + "net/http/httputil" "strconv" + "strings" "time" + "git.handmade.network/hmn/hmn/src/buildscss" "git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/email" "git.handmade.network/hmn/hmn/src/hmndata" "git.handmade.network/hmn/hmn/src/hmnurl" + "git.handmade.network/hmn/hmn/src/logging" "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/utils" @@ -37,8 +43,53 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { redirectToHMN, ) + routes.GET(hmnurl.RegexEsBuild, func(c *RequestContext) ResponseData { + if buildscss.ActiveServerPort != 0 { + var res ResponseData + proxy := httputil.ReverseProxy{ + Director: func(r *http.Request) { + r.URL.Scheme = "http" + r.URL.Host = fmt.Sprintf("localhost:%d", buildscss.ActiveServerPort) + r.Host = "localhost" + }, + } + logging.Debug().Msg("Redirecting esbuild SSE request to esbuild") + proxy.ServeHTTP(c.Res, c.Req) + res.hijacked = true + return res + } + return FourOhFour(c) + }) + routes.GET(hmnurl.RegexPublic, func(c *RequestContext) ResponseData { var res ResponseData + if buildscss.ActiveServerPort != 0 { + if strings.HasSuffix(c.Req.URL.Path, ".css") { + proxy := httputil.ReverseProxy{ + Director: func(r *http.Request) { + r.URL.Scheme = "http" + r.URL.Host = fmt.Sprintf("localhost:%d", buildscss.ActiveServerPort) + r.Host = "localhost" + }, + ModifyResponse: func(res *http.Response) error { + if res.StatusCode > 400 { + errStr, err := io.ReadAll(res.Body) + if err != nil { + return err + } + res.Body.Close() + logging.Error().Str("EsBuild error", string(errStr)).Msg("EsBuild is complaining") + res.Body = io.NopCloser(bytes.NewReader(errStr)) + } + return nil + }, + } + logging.Debug().Msg("Redirecting css request to esbuild") + proxy.ServeHTTP(c.Res, c.Req) + res.hijacked = true + return res + } + } http.StripPrefix("/public/", http.FileServer(http.Dir("public"))).ServeHTTP(&res, c.Req) addCORSHeaders(c, &res) return res diff --git a/src/website/website.go b/src/website/website.go index b411ef25..904f796c 100644 --- a/src/website/website.go +++ b/src/website/website.go @@ -12,6 +12,7 @@ import ( "git.handmade.network/hmn/hmn/src/assets" "git.handmade.network/hmn/hmn/src/auth" + "git.handmade.network/hmn/hmn/src/buildscss" "git.handmade.network/hmn/hmn/src/calendar" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/db" @@ -54,6 +55,7 @@ var WebsiteCommand = &cobra.Command{ hmns3.StartServer(backgroundJobContext), assets.BackgroundPreviewGeneration(backgroundJobContext, conn), calendar.MonitorCalendars(backgroundJobContext), + buildscss.RunServer(backgroundJobContext), ) signals := make(chan os.Signal, 1) @@ -64,10 +66,10 @@ var WebsiteCommand = &cobra.Command{ go func() { timeout, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - logging.Info().Msg("shutting down web server") - server.Shutdown(timeout) logging.Info().Msg("cancelling background jobs") cancelBackgroundJobs() + logging.Info().Msg("shutting down web server") + server.Shutdown(timeout) }() <-signals