Skip to content

Instantly share code, notes, and snippets.

@hasenj
Created March 8, 2023 06:19
Show Gist options
  • Save hasenj/1b06ca1d5749e2045dff57b2f0fa67a0 to your computer and use it in GitHub Desktop.
Save hasenj/1b06ca1d5749e2045dff57b2f0fa67a0 to your computer and use it in GitHub Desktop.
A little js/ts bundler based on esbuild (assumes preact, change to suit your needs). CC0/mit-0/unlicense
package esbuilder
import "os"
import "path"
import "fmt"
import "log"
import "time"
// the main hero!
import "github.com/evanw/esbuild/pkg/api"
// so we can support watch mode
import "github.com/fsnotify/fsnotify"
// for copying files and directories as-is
import "github.com/otiai10/copy"
import "github.com/fatih/color"
type Options struct {
Minify bool
EntryPoints []string
Define map[string]string
Outdir string
CopyRoot string // the root directory that the items to copy are relative to
CopyItems []string // files and directories to copy as-is to the output directory
}
func Build(options Options) bool {
t0 := time.Now()
esbOptions := api.BuildOptions{
EntryPoints: options.EntryPoints,
Charset: api.CharsetUTF8,
Bundle: true,
Outdir: options.Outdir,
MinifyWhitespace: false,
MinifyIdentifiers: false,
MinifySyntax: false,
Sourcemap: api.SourceMapLinked,
LogLimit: 3,
JSXFactory: "preact.h",
JSXFragment: "preact.Fragment",
Define: options.Define,
Write: true,
}
if options.Minify {
esbOptions.MinifyWhitespace = true
esbOptions.MinifyIdentifiers = true
esbOptions.MinifySyntax = true
esbOptions.Sourcemap = api.SourceMapNone
}
os.RemoveAll(options.Outdir)
cleanTime := time.Since(t0).Round(time.Millisecond)
t1 := time.Now()
result := api.Build(esbOptions)
buildTime := time.Since(t1).Round(time.Millisecond)
if len(result.Errors) > 0 {
msgColor := color.New(color.FgRed, color.Bold)
locColor := color.New(color.FgMagenta)
contentColor := color.New(color.FgHiCyan)
contentErrorColor := color.New(color.FgHiRed)
errorCursorColor := color.New(color.FgHiRed)
for _, err := range result.Errors {
if err.Location != nil {
locColor.Printf("%s:%d:%d: ", err.Location.File, err.Location.Line, err.Location.Column)
}
msgColor.Println(err.Text)
if err.Location != nil {
fmt.Println()
fmt.Print(" ")
textBefore := err.Location.LineText[:err.Location.Column]
textError := err.Location.LineText[err.Location.Column : err.Location.Column+err.Location.Length]
textAfter := err.Location.LineText[err.Location.Column+err.Location.Length:]
contentColor.Print(textBefore)
contentErrorColor.Print(textError)
contentColor.Print(textAfter)
fmt.Println()
fmt.Print(" ")
for _ = range textBefore {
fmt.Print(" ")
}
for _ = range textError {
errorCursorColor.Print("^")
}
fmt.Println()
}
}
return false
}
t2 := time.Now()
// copy some files as-is
var copyErrors []error
for _, item := range options.CopyItems {
err := copy.Copy(path.Join(options.CopyRoot, item), path.Join(options.Outdir, item))
if err != nil {
copyErrors = append(copyErrors, err)
}
}
if len(copyErrors) > 0 {
fmt.Println("Copying Errors:")
for _, err := range copyErrors {
fmt.Println(err)
}
return false
}
copyTime := time.Since(t2).Round(time.Millisecond)
fmt.Println("Frontend Build Time:", time.Since(t0).Round(time.Millisecond))
return true
}
func WatchFrontend(options Options, watchDirs ...string) {
callback := func() {
log.Println("Building frontend")
Build(options)
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
fmt.Println("Watching is not supported:", err)
return
}
for _, dir := range watchDirs {
err = watcher.Add(dir)
if err != nil {
fmt.Println("Error watching directory", dir)
fmt.Println("\t", err)
return
}
}
// build once at first
callback()
var next time.Time
var waitTime = 10 * time.Millisecond
for event := range watcher.Events {
if !(event.Op == fsnotify.Write || event.Op == fsnotify.Create) {
// we don't care about these other events
continue
}
var now = time.Now()
if event.Op == fsnotify.Create {
watcher.Add(event.Name)
}
if now.Before(next) {
// fmt.Println("skipping file event handler; next is scheduled at", next)
continue
}
next = now.Add(waitTime)
time.AfterFunc(waitTime, callback)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment