From b70706948575921a9dc2788f5607a474a4536b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Marussy?= Date: Tue, 24 Dec 2024 17:20:48 +0100 Subject: [PATCH] feat(language-web): serve compressed assets --- subprojects/frontend/package.json | 3 +- .../frontend/scripts/compressFiles.mjs | 64 +++++++++++++++++++ subprojects/frontend/tsconfig.node.json | 1 + .../refinery/language/web/ServerLauncher.java | 1 + yarn.lock | 1 + 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 subprojects/frontend/scripts/compressFiles.mjs diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index b28a3bd11..cb203f7ef 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json @@ -10,7 +10,7 @@ "type": "module", "private": true, "scripts": { - "build": "MODE=production vite build", + "build": "MODE=production vite build && node scripts/compressFiles.mjs", "dev": "MODE=development vite serve", "typegen": "xstate typegen \"src/**/*.ts?(x)\"", "typecheck": "yarn run g:tsc -p subprojects/frontend/tsconfig.shared.json && yarn run g:tsc -p subprojects/frontend/tsconfig.node.json && yarn run g:tsc -p subprojects/frontend/tsconfig.json", @@ -101,6 +101,7 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react-swc": "^3.7.2", "@xstate/cli": "^0.5.17", + "fast-glob": "^3.3.2", "html-minifier-terser": "^7.2.0", "micromatch": "^4.0.8", "pnpapi": "^0.0.0", diff --git a/subprojects/frontend/scripts/compressFiles.mjs b/subprojects/frontend/scripts/compressFiles.mjs new file mode 100644 index 000000000..46ef0595f --- /dev/null +++ b/subprojects/frontend/scripts/compressFiles.mjs @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import zlib from 'node:zlib'; + +import fg from 'fast-glob'; + +const brotliCompress = promisify(zlib.brotliCompress); + +const gzip = promisify(zlib.gzip); + +export const minRatio = 0.8; + +/** @type {import('node:zlib').ZlibOptions} */ +export const gzipOptions = { + level: 9, +}; + +/** @type {import('node:zlib').BrotliOptions} */ +export const brotliOptions = { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 11, + }, +}; + +async function compressFiles() { + const cwd = path.resolve(import.meta.dirname, '../build/vite/production'); + const inputFiles = await fg( + '**/*.{css,html,js,license,mjs,svg,txt,webmanifest}', + { cwd }, + ); + const promises = inputFiles.map(async (inputFile) => { + const absoluteInputFile = path.resolve(cwd, inputFile); + const contents = await readFile(absoluteInputFile); + if (contents.length <= 1500) { + // Don't compress files smaller than the TCP MTU. + } + const maxLength = Math.floor(contents.length * minRatio); + await Promise.all([ + (async () => { + const gzipContents = await gzip(contents, gzipOptions); + if (gzipContents.length <= maxLength) { + await writeFile(`${absoluteInputFile}.gz`, gzipContents); + } + })(), + (async () => { + const brotliContents = await brotliCompress(contents, brotliOptions); + if (brotliContents.length <= maxLength) { + await writeFile(`${absoluteInputFile}.br`, brotliContents); + } + })(), + ]); + }); + await Promise.all(promises); +} + +// @ts-expect-error We can use top-level await here, because this file is only run by Node.js. +await compressFiles(); diff --git a/subprojects/frontend/tsconfig.node.json b/subprojects/frontend/tsconfig.node.json index 99e4eaca2..9dfaeed1f 100644 --- a/subprojects/frontend/tsconfig.node.json +++ b/subprojects/frontend/tsconfig.node.json @@ -17,6 +17,7 @@ "config/*.ts", "config/*.cjs", "prettier.config.cjs", + "scripts/*.mjs", "types/node", "vite.config.ts" ], diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index 155efc6fe..762a87ea2 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -100,6 +100,7 @@ private void addDefaultServlet(ServletContextHandler handler) { // See also the related Jetty ticket: // https://github.com/eclipse/jetty.project/issues/2925 defaultServletHolder.setInitParameter("useFileMappedBuffer", isWindows ? "false" : "true"); + defaultServletHolder.setInitParameter("precompressed", "br=.br,gzip=.gz"); handler.addServlet(defaultServletHolder, "/"); } diff --git a/yarn.lock b/yarn.lock index 426e20b97..0076f9057 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3975,6 +3975,7 @@ __metadata: direction: "npm:^2.0.1" dompurify: "npm:^3.2.3" escape-string-regexp: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" html-minifier-terser: "npm:^7.2.0" jspdf: "npm:^2.5.2" lodash-es: "npm:^4.17.21"