sketch: compress JS and CSS
We noticed that our JS bundles weren't compressed; let's compress them.
Sketch did most of the work here itself, but there's some nonsense
around the fact that we pass a zip.Reader around, which can't seek.
We're probably doing more work than strictly necessary on the server side.
Add gzip compression for JS and source map files in esbuild
- Added compression for .js and .js.map files in the esbuild Build() function
- Gzipped files are created alongside the originals with .gz extension
- This allows for compressed asset serving when supported by clients
Co-Authored-By: sketch
Add support for serving pre-compressed JS and source map files
- Created a custom HTTP handler for /static/ path
- Handler checks if requested JS/JS.map files have gzipped versions
- Serves gzipped files with proper Content-Encoding headers when available
- Falls back to original files when compressed versions are not found
- Client support for gzip encoding is verified through Accept-Encoding header
Co-Authored-By: sketch
Extend gzip compression to include CSS files
- Added CSS files to the compression list alongside JS and source map files
- Added debug output to show which files are being compressed
- Updated error messages to reflect the inclusion of CSS files
Co-Authored-By: sketch
Fix variable naming in Accept-Encoding header processing
- Renamed 'header' variable to 'encoding' for better semantics when processing Accept-Encoding headers
- Addresses gopls check issue in compressed file handler
Co-Authored-By: sketch
Simplify Accept-Encoding header processing
- Simplified check for gzip support using strings.Contains()
- More efficient approach than splitting and iterating
- Addresses gopls efficiency recommendation
Co-Authored-By: sketch
Extract compressed file handler to separate package
- Created a new package loop/server/gzhandler
- Moved compressedFileHandler implementation to the new package
- Renamed to Handler for better Go idioms
- Updated loophttp.go to use the new package
- Improved modularity and separation of concerns
Co-Authored-By: sketch
Enhance gzhandler and add test coverage
- Updated gzhandler to handle all files except .gz files
- Added comprehensive test suite for gzhandler
- Removed debug print from esbuild.go
- Tests different file types, browsers with and without gzip support
- Tests directory handling
Co-Authored-By: sketch
Fix 'seeker can't seek' error in gzhandler
- Changed approach to read gzipped file into memory before serving
- Avoids io.Seeker interface requirement for http.ServeContent
- Fixes 500 error when serving compressed JavaScript files
- Added missing io import
Co-Authored-By: sketch
diff --git a/loop/webui/esbuild.go b/loop/webui/esbuild.go
index d4af636..03c745e 100644
--- a/loop/webui/esbuild.go
+++ b/loop/webui/esbuild.go
@@ -1,12 +1,11 @@
// Package webui provides the web interface for the sketch loop.
// It bundles typescript files into JavaScript using esbuild.
-//
-// This is substantially the same mechanism as /esbuild.go in this repo as well.
package webui
import (
"archive/zip"
"bytes"
+ "compress/gzip"
"crypto/sha256"
"embed"
"encoding/hex"
@@ -204,6 +203,57 @@
return nil, fmt.Errorf("failed to write xterm.css: %w", err)
}
+ // Compress all .js, .js.map, and .css files with gzip, leaving the originals in place
+ err = filepath.Walk(tmpHashDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ // Check if file is a .js or .js.map file
+ if !strings.HasSuffix(path, ".js") && !strings.HasSuffix(path, ".js.map") && !strings.HasSuffix(path, ".css") {
+ return nil
+ }
+
+ // Read the original file
+ origData, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("failed to read file %s: %w", path, err)
+ }
+
+ // Create a gzipped file
+ gzipPath := path + ".gz"
+ gzipFile, err := os.Create(gzipPath)
+ if err != nil {
+ return fmt.Errorf("failed to create gzip file %s: %w", gzipPath, err)
+ }
+ defer gzipFile.Close()
+
+ // Create a gzip writer
+ gzWriter := gzip.NewWriter(gzipFile)
+ defer gzWriter.Close()
+
+ // Write the original file content to the gzip writer
+ _, err = gzWriter.Write(origData)
+ if err != nil {
+ return fmt.Errorf("failed to write to gzip file %s: %w", gzipPath, err)
+ }
+
+ // Ensure we flush and close properly
+ if err := gzWriter.Close(); err != nil {
+ return fmt.Errorf("failed to close gzip writer for %s: %w", gzipPath, err)
+ }
+ if err := gzipFile.Close(); err != nil {
+ return fmt.Errorf("failed to close gzip file %s: %w", gzipPath, err)
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to compress .js/.js.map/.css files: %w", err)
+ }
+
// Everything succeeded, so we write tmpHashDir to hashZip
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)