diff --git a/package-lock.json b/package-lock.json index ec62c6f..b7778d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -146,7 +146,8 @@ "tailwindcss": "^4.1.14", "tsx": "^4.20.5", "typescript": "5.6.3", - "vite": "^7.1.9" + "vite": "^7.1.9", + "vite-plugin-compression2": "^2.5.3" }, "optionalDependencies": { "bufferutil": "^4.0.8" @@ -5515,6 +5516,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", @@ -9660,6 +9684,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -13631,6 +13662,13 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-mini": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz", + "integrity": "sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -14133,6 +14171,17 @@ } } }, + "node_modules/vite-plugin-compression2": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.5.3.tgz", + "integrity": "sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "tar-mini": "^0.2.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/package.json b/package.json index d4db575..c008136 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,8 @@ "tailwindcss": "^4.1.14", "tsx": "^4.20.5", "typescript": "5.6.3", - "vite": "^7.1.9" + "vite": "^7.1.9", + "vite-plugin-compression2": "^2.5.3" }, "optionalDependencies": { "bufferutil": "^4.0.8" diff --git a/server/static.ts b/server/static.ts index b3a7c13..af973ef 100644 --- a/server/static.ts +++ b/server/static.ts @@ -1,7 +1,6 @@ import express, { type Express } from "express"; import fs from "fs"; import path from "path"; -import compression from "compression"; export function serveStatic(app: Express) { const distPath = path.resolve(__dirname, "public"); @@ -11,29 +10,48 @@ export function serveStatic(app: Express) { ); } - // Enable gzip compression for static assets - // Caddy/Traefik devem respeitar isso ou fazer override - app.use(compression({ - level: 6, - threshold: 1024, // Compress files > 1KB - filter: (req, res) => { - // Don't compress if already compressed by proxy - if (req.headers['x-no-compression']) { - return false; + // Middleware to serve pre-compressed .gz files + app.use((req, res, next) => { + const acceptEncoding = req.headers["accept-encoding"] || ""; + + // Only handle GET/HEAD requests + if (req.method !== "GET" && req.method !== "HEAD") { + return next(); + } + + // Check if client accepts gzip + if (!acceptEncoding.includes("gzip")) { + return next(); + } + + const url = req.url; + // Only handle JS, CSS, and HTML files + if (!url.match(/\.(js|css|html?)$/)) { + return next(); + } + + const gzPath = path.join(distPath, url + ".gz"); + + if (fs.existsSync(gzPath)) { + // Set headers for gzip response + res.setHeader("Content-Encoding", "gzip"); + res.setHeader("Vary", "Accept-Encoding"); + + // Set correct content type + if (url.endsWith(".js")) { + res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); + } else if (url.endsWith(".css")) { + res.setHeader("Content-Type", "text/css; charset=UTF-8"); + } else if (url.match(/\.html?$/)) { + res.setHeader("Content-Type", "text/html; charset=UTF-8"); } - // Compress JS, CSS, JSON, HTML - const contentType = res.getHeader('content-type') || ''; - if (typeof contentType === 'string') { - if (contentType.includes('javascript') || - contentType.includes('css') || - contentType.includes('json') || - contentType.includes('html')) { - return true; - } - } - return compression.filter(req, res); - }, - })); + + // Serve the gzipped file + return res.sendFile(gzPath); + } + + next(); + }); // Serve static files with proper caching app.use(express.static(distPath, { diff --git a/vite.config.ts b/vite.config.ts index 4a5aca4..c2d40c3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,7 @@ import tailwindcss from "@tailwindcss/vite"; import path from "path"; import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; import { metaImagesPlugin } from "./vite-plugin-meta-images"; +import { compression } from "vite-plugin-compression2"; export default defineConfig({ plugins: [ @@ -11,6 +12,14 @@ export default defineConfig({ runtimeErrorOverlay(), tailwindcss(), metaImagesPlugin(), + compression({ + algorithm: "gzip", + exclude: [/\.(br)$/, /\.(gz)$/], + threshold: 1024, + compressionOptions: { + level: 6, + }, + }), ...(process.env.NODE_ENV !== "production" && process.env.REPL_ID !== undefined ? [