Merge pull request #73 from lordmathis/fix/compression

Fix asset compression
This commit is contained in:
2025-11-04 19:17:20 +01:00
committed by GitHub
5 changed files with 105 additions and 24 deletions

12
app/package-lock.json generated
View File

@@ -56,7 +56,7 @@
"sass": "^1.80.4", "sass": "^1.80.4",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.4.1", "vite": "^6.4.1",
"vite-plugin-compression2": "^1.3.0", "vite-plugin-compression2": "^2.3.1",
"vitest": "^3.1.4" "vitest": "^3.1.4"
} }
}, },
@@ -9794,15 +9794,11 @@
} }
}, },
"node_modules/vite-plugin-compression2": { "node_modules/vite-plugin-compression2": {
"version": "1.3.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-1.3.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.3.1.tgz",
"integrity": "sha512-/cYzISoYOo/SwPUBReS1E02a8eNTpQm8+lQUBj5NNGxuq4iZ3JOfWExUlobhVhPMJuejD7dipT+cMLbaWsMbdw==", "integrity": "sha512-bnhLTsurtvOiiP6EMISIKVsOMCeTAjE6FJbyqQus3W4mtAxF7pCuC4puUIAiCgNs98tOCpqo6GIXJXTLufzIaw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"workspaces": [
"example",
"e2e/*"
],
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.1.0", "@rollup/pluginutils": "^5.1.0",
"tar-mini": "^0.2.0" "tar-mini": "^0.2.0"

View File

@@ -76,7 +76,7 @@
"sass": "^1.80.4", "sass": "^1.80.4",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.4.1", "vite": "^6.4.1",
"vite-plugin-compression2": "^1.3.0", "vite-plugin-compression2": "^2.3.1",
"vitest": "^3.1.4" "vitest": "^3.1.4"
}, },
"browserslist": { "browserslist": {

View File

@@ -11,7 +11,10 @@ export default defineConfig(({ mode }) => ({
react({ react({
include: ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'], include: ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'],
}), }),
compression(), compression({
threshold: 1024, // Only compress files > 1KB
deleteOriginalAssets: false, // Keep original files
}),
], ],
root: 'src', root: 'src',

View File

@@ -24,6 +24,28 @@ func getStaticLogger() logging.Logger {
return logging.WithGroup("static") return logging.WithGroup("static")
} }
// getContentType returns the appropriate content type based on file extension
func getContentType(path string) string {
switch filepath.Ext(path) {
case ".js":
return "application/javascript"
case ".css":
return "text/css"
case ".html":
return "text/html"
case ".json":
return "application/json"
case ".svg":
return "image/svg+xml"
case ".xml":
return "application/xml"
case ".yaml", ".yml":
return "application/x-yaml"
default:
return "application/octet-stream"
}
}
// ServeHTTP serves the static files // ServeHTTP serves the static files
func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log := getStaticLogger().With( log := getStaticLogger().With(
@@ -77,23 +99,28 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// Check for pre-compressed version // Check for pre-compressed versions (prefer brotli over gzip)
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { acceptEncoding := r.Header.Get("Accept-Encoding")
// Try brotli first (better compression ratio)
if strings.Contains(acceptEncoding, "br") {
brPath := cleanPath + ".br"
if _, err := os.Stat(brPath); err == nil {
w.Header().Set("Content-Encoding", "br")
w.Header().Set("Content-Type", getContentType(cleanPath))
w.Header().Set("Vary", "Accept-Encoding")
http.ServeFile(w, r, brPath)
return
}
}
// Fall back to gzip
if strings.Contains(acceptEncoding, "gzip") {
gzPath := cleanPath + ".gz" gzPath := cleanPath + ".gz"
if _, err := os.Stat(gzPath); err == nil { if _, err := os.Stat(gzPath); err == nil {
w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Type", getContentType(cleanPath))
// Set proper content type based on original file w.Header().Set("Vary", "Accept-Encoding")
contentType := "application/octet-stream"
switch filepath.Ext(cleanPath) {
case ".js":
contentType = "application/javascript"
case ".css":
contentType = "text/css"
case ".html":
contentType = "text/html"
}
w.Header().Set("Content-Type", contentType)
http.ServeFile(w, r, gzPath) http.ServeFile(w, r, gzPath)
return return
} }

View File

@@ -26,8 +26,12 @@ func TestStaticHandler_Integration(t *testing.T) {
"index.html": []byte("<html><body>Index</body></html>"), "index.html": []byte("<html><body>Index</body></html>"),
"assets/style.css": []byte("body { color: blue; }"), "assets/style.css": []byte("body { color: blue; }"),
"assets/style.css.gz": []byte("gzipped css content"), "assets/style.css.gz": []byte("gzipped css content"),
"assets/style.css.br": []byte("brotli css content"),
"assets/script.js": []byte("console.log('test');"), "assets/script.js": []byte("console.log('test');"),
"assets/script.js.gz": []byte("gzipped js content"), "assets/script.js.gz": []byte("gzipped js content"),
"assets/script.js.br": []byte("brotli js content"),
"assets/app.js": []byte("console.log('app');"),
"assets/app.js.br": []byte("brotli app content"),
"subdir/page.html": []byte("<html><body>Page</body></html>"), "subdir/page.html": []byte("<html><body>Page</body></html>"),
"subdir/page.html.gz": []byte("gzipped html content"), "subdir/page.html.gz": []byte("gzipped html content"),
} }
@@ -52,6 +56,7 @@ func TestStaticHandler_Integration(t *testing.T) {
wantType string wantType string
wantEncoding string wantEncoding string
wantCacheHeader string wantCacheHeader string
wantVary string
}{ }{
{ {
name: "serve index.html", name: "serve index.html",
@@ -69,6 +74,7 @@ func TestStaticHandler_Integration(t *testing.T) {
wantType: "text/css", wantType: "text/css",
wantEncoding: "gzip", wantEncoding: "gzip",
wantCacheHeader: "public, max-age=31536000", wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
}, },
{ {
name: "serve JS with gzip support", name: "serve JS with gzip support",
@@ -79,6 +85,7 @@ func TestStaticHandler_Integration(t *testing.T) {
wantType: "application/javascript", wantType: "application/javascript",
wantEncoding: "gzip", wantEncoding: "gzip",
wantCacheHeader: "public, max-age=31536000", wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
}, },
{ {
name: "serve CSS without gzip", name: "serve CSS without gzip",
@@ -114,6 +121,50 @@ func TestStaticHandler_Integration(t *testing.T) {
wantBody: []byte("<html><body>Index</body></html>"), wantBody: []byte("<html><body>Index</body></html>"),
wantType: "text/html; charset=utf-8", wantType: "text/html; charset=utf-8",
}, },
{
name: "serve CSS with brotli support",
path: "/assets/style.css",
acceptEncoding: "br",
wantStatus: http.StatusOK,
wantBody: []byte("brotli css content"),
wantType: "text/css",
wantEncoding: "br",
wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
},
{
name: "serve JS with brotli support",
path: "/assets/script.js",
acceptEncoding: "br",
wantStatus: http.StatusOK,
wantBody: []byte("brotli js content"),
wantType: "application/javascript",
wantEncoding: "br",
wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
},
{
name: "prefer brotli over gzip when both supported",
path: "/assets/script.js",
acceptEncoding: "gzip, br",
wantStatus: http.StatusOK,
wantBody: []byte("brotli js content"),
wantType: "application/javascript",
wantEncoding: "br",
wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
},
{
name: "fallback to gzip when brotli not available",
path: "/assets/app.js",
acceptEncoding: "gzip, br",
wantStatus: http.StatusOK,
wantBody: []byte("brotli app content"),
wantType: "application/javascript",
wantEncoding: "br",
wantCacheHeader: "public, max-age=31536000",
wantVary: "Accept-Encoding",
},
} }
for _, tc := range tests { for _, tc := range tests {
@@ -139,6 +190,10 @@ func TestStaticHandler_Integration(t *testing.T) {
if tc.wantCacheHeader != "" { if tc.wantCacheHeader != "" {
assert.Equal(t, tc.wantCacheHeader, w.Header().Get("Cache-Control")) assert.Equal(t, tc.wantCacheHeader, w.Header().Get("Cache-Control"))
} }
if tc.wantVary != "" {
assert.Equal(t, tc.wantVary, w.Header().Get("Vary"))
}
} }
}) })
} }