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