Added more performant minifiers for css and js.

This commit is contained in:
SamTV12345 2024-03-21 10:04:08 +01:00
parent 3a1ef560ec
commit eec1dd18d2
8 changed files with 87 additions and 112 deletions

View file

@ -6,12 +6,6 @@
"shutdown": "ep_etherpad-lite/node/db/DB"
}
},
{
"name": "Minify",
"hooks": {
"shutdown": "ep_etherpad-lite/node/utils/Minify"
}
},
{
"name": "express",
"hooks": {

View file

@ -28,6 +28,8 @@ let sessionStore: { shutdown: () => void; } | null;
const sockets:Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime');
import https from 'https';
import http from 'http';
exports.server = null;
@ -119,11 +121,8 @@ exports.restartServer = async () => {
options.ca.push(fs.readFileSync(caFileName));
}
}
const https = require('https');
exports.server = https.createServer(options, app);
} else {
const http = require('http');
exports.server = http.createServer(app);
}

View file

@ -4,8 +4,8 @@ import {MapArrayType} from "../../types/MapType";
import {PartType} from "../../types/PartType";
const fs = require('fs').promises;
const minify = require('../../utils/Minify');
const path = require('path');
import {requestURIs, minify} from '../../utils/Minify';
import path from 'path';
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../utils/Settings');
import CachingMiddleware from '../../utils/caching_middleware';
@ -41,7 +41,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
app.all('/static/:filename(*)', minify.minify);
app.all('/static/:filename(*)', minify);
// Setup middleware that will package JavaScript files served by minify for
// CommonJS loader on the client-side.
@ -51,7 +51,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
rootURI: 'http://invalid.invalid/static/js/',
libraryPath: 'javascripts/lib/',
libraryURI: 'http://invalid.invalid/static/plugins/',
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
requestURIs: requestURIs, // Loop-back is causing problems, this is a workaround.
});
const StaticAssociator = Yajsml.associators.StaticAssociator;

View file

@ -31,11 +31,13 @@ import axios from "axios";
const settings = require('./utils/Settings');
import wtfnodeMod from 'wtfnode';
let wtfnode: any;
if (settings.dumpOnUncleanExit) {
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and
// it should be above everything else so that it can hook in before resources are used.
wtfnode = require('wtfnode');
wtfnode = wtfnodeMod;
}

View file

@ -21,21 +21,22 @@
* limitations under the License.
*/
import {compressCSS, compressJS} from "./MinifyWorker";
import {MapArrayType} from "../types/MapType";
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
import {promises as fs} from 'fs'
import path from 'path';
const plugins = require('../../static/js/pluginfw/plugin_defs');
const RequireKernel = require('etherpad-require-kernel');
const mime = require('mime-types');
const Threads = require('threads');
const log4js = require('log4js');
import mime from 'mime-types';
import log4js from 'log4js';
const sanitizePathname = require('./sanitizePathname');
const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.join(settings.root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const LIBRARY_WHITELIST = [
'async',
@ -49,10 +50,10 @@ const LIBRARY_WHITELIST = [
// What follows is a terrible hack to avoid loop-back within the server.
// TODO: Serve files from another service, or directly from the file system.
const requestURI = async (url, method, headers) => {
export const requestURI = async (url: string, method: string, headers: MapArrayType<any>) => {
const parsedUrl = new URL(url);
let status = 500;
const content = [];
const content: string[] = [];
const mockRequest = {
url,
method,
@ -62,7 +63,7 @@ const requestURI = async (url, method, headers) => {
let mockResponse;
const p = new Promise((resolve) => {
mockResponse = {
writeHead: (_status, _headers) => {
writeHead: (_status: number, _headers: MapArrayType<any>) => {
status = _status;
for (const header in _headers) {
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
@ -70,16 +71,16 @@ const requestURI = async (url, method, headers) => {
}
}
},
setHeader: (header, value) => {
setHeader: (header: string, value: string) => {
headers[header.toLowerCase()] = value.toString();
},
header: (header, value) => {
header: (header: string, value: string) => {
headers[header.toLowerCase()] = value.toString();
},
write: (_content) => {
write: (_content: string) => {
_content && content.push(_content);
},
end: (_content) => {
end: (_content: string) => {
_content && content.push(_content);
resolve([status, headers, content.join('')]);
},
@ -89,16 +90,17 @@ const requestURI = async (url, method, headers) => {
return await p;
};
const requestURIs = (locations, method, headers, callback) => {
export const requestURIs = (locations: string[], method: string, headers: string[], callback:Function) => {
Promise.all(locations.map(async (loc) => {
try {
return await requestURI(loc, method, headers);
} catch (err) {
} catch (err:any) {
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
return [500, headers, ''];
}
})).then((responses) => {
// @ts-ignore
})).then((responses:[[number,number,number]]) => {
const statuss = responses.map((x) => x[0]);
const headerss = responses.map((x) => x[1]);
const contentss = responses.map((x) => x[2]);
@ -120,11 +122,11 @@ const compatPaths = {
* @param req the Express request
* @param res the Express response
*/
const minify = async (req, res) => {
export const minify = async (req: any, res:any) => {
let filename = req.params.filename;
try {
filename = sanitizePathname(filename);
} catch (err) {
} catch (err:any) {
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
res.writeHead(404, {});
res.end();
@ -132,6 +134,7 @@ const minify = async (req, res) => {
}
// Backward compatibility for plugins that require() files from old paths.
// @ts-ignore
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
if (newLocation != null) {
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
@ -169,7 +172,7 @@ const minify = async (req, res) => {
const [, testf] = /^plugins\/ep_etherpad-lite\/(tests\/frontend\/.*)/.exec(filename) || [];
if (testf != null) filename = `../${testf}`;
const contentType = mime.lookup(filename);
const contentType = mime.lookup(filename) as string;
const [date, exists] = await statFile(filename, 3);
if (date) {
@ -186,7 +189,7 @@ const minify = async (req, res) => {
if (!exists) {
res.writeHead(404, {});
res.end();
} else if (new Date(req.headers['if-modified-since']) >= date) {
} else if (new Date(req.headers['if-modified-since']) >= date!) {
res.writeHead(304, {});
res.end();
} else if (req.method === 'HEAD') {
@ -206,7 +209,7 @@ const minify = async (req, res) => {
};
// Check for the existance of the file and get the last modification date.
const statFile = async (filename, dirStatLimit) => {
const statFile = async (filename: string, dirStatLimit: number):Promise<[Date|null, boolean]> => {
/*
* The only external call to this function provides an explicit value for
* dirStatLimit: this check could be removed.
@ -227,9 +230,10 @@ const statFile = async (filename, dirStatLimit) => {
let stats;
try {
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
} catch (err) {
} catch (err:any) {
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
// Stat the directory instead.
// @ts-ignore
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
return [date, false];
}
@ -241,7 +245,7 @@ const statFile = async (filename, dirStatLimit) => {
const lastModifiedDateOfEverything = async () => {
const folders2check = [path.join(ROOT_DIR, 'js/'), path.join(ROOT_DIR, 'css/')];
let latestModification = null;
let latestModification:Date|null = null;
// go through this two folders
await Promise.all(folders2check.map(async (dir) => {
// read the files in the folder
@ -251,7 +255,7 @@ const lastModifiedDateOfEverything = async () => {
files.push('.');
// go through all files in this folder
await Promise.all(files.map(async (filename) => {
await Promise.all(files.map(async (filename: string) => {
// get the stat data of this file
const stats = await fs.stat(path.join(dir, filename));
@ -269,58 +273,39 @@ const lastModifiedDateOfEverything = async () => {
const _requireLastModified = new Date();
const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`;
const getFileCompressed = async (filename, contentType) => {
const getFileCompressed = async (filename: string, contentType: string) => {
let content = await getFile(filename);
if (!content || !settings.minify) {
return content;
} else if (contentType === 'application/javascript') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressJS}) => {
try {
logger.info('Compress JS file %s.', filename);
try {
logger.info('Compress JS file %s.', filename);
content = content.toString();
const compressResult = await compressJS(content);
content = content.toString();
const compressResult = compressJS(content);
if (compressResult.error) {
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
} else {
content = compressResult.code.toString(); // Convert content obj code to string
}
} catch (error) {
console.error('getFile() returned an error in ' +
content = compressResult.code.toString(); // Convert content obj code to string
} catch (error) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${error}`);
}
resolve(content);
});
});
}
return content
} else if (contentType === 'text/css') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressCSS}) => {
try {
logger.info('Compress CSS file %s.', filename);
content = await compressCSS(filename, ROOT_DIR);
} catch (error) {
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
}
resolve(content);
});
});
try {
logger.info('Compress CSS file %s.', filename);
content = compressCSS(filename, ROOT_DIR, Buffer.from(content)).toString();
return content
} catch (error) {
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
}
} else {
return content;
}
};
const getFile = async (filename) => {
const getFile = async (filename: string) => {
if (filename === 'js/require-kernel.js') return requireDefinition();
return await fs.readFile(path.resolve(ROOT_DIR, filename));
};
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
exports.requestURIs = requestURIs;
exports.shutdown = async (hookName, context) => {
await threadsPool.terminate();
};
exports.minify = (req: Request, res: Response, next: Function) => minify(req, res).catch((err) => next(err || new Error(err)));

View file

@ -1,33 +0,0 @@
'use strict';
/**
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
const CleanCSS = require('clean-css');
const Terser = require('terser');
const fsp = require('fs').promises;
const path = require('path');
const Threads = require('threads');
const compressJS = (content) => Terser.minify(content);
const compressCSS = async (filename, ROOT_DIR) => {
const absPath = path.resolve(ROOT_DIR, filename);
try {
const basePath = path.dirname(absPath);
const output = await new CleanCSS({
rebase: true,
rebaseTo: basePath,
}).minify([absPath]);
return output.styles;
} catch (error) {
// on error, just yield the un-minified original, but write a log message
console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`);
return await fsp.readFile(absPath, 'utf8');
}
};
Threads.expose({
compressJS,
compressCSS,
});

View file

@ -0,0 +1,29 @@
'use strict';
/**
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
import {promises as fsp} from "fs";
import path from 'path';
import {minifySync} from "@swc/core";
import lightminify from 'lightningcss';
export const compressJS = (content: string) => minifySync(content);
export const compressCSS = (filename: string, ROOT_DIR: string, content: Buffer) => {
const absPath = path.resolve(ROOT_DIR, filename);
try {
const basePath = path.dirname(absPath);
let { code } = lightminify.transform({
filename: absPath,
minify: true,
code: content
});
return code;
} catch (error) {
// on error, just yield the un-minified original, but write a log message
console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`);
return content.toString()
}
};

View file

@ -30,9 +30,9 @@
}
],
"dependencies": {
"@swc/core": "^1.4.8",
"async": "^3.2.5",
"axios": "^1.7.2",
"clean-css": "^5.3.3",
"cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.10",
@ -51,6 +51,7 @@
"jsonminify": "0.4.2",
"jsonwebtoken": "^9.0.2",
"languages4translatewiki": "0.1.3",
"lightningcss": "^1.24.1",
"live-plugin-manager": "^1.0.0",
"lodash.clonedeep": "4.5.0",
"log4js": "^6.9.1",
@ -68,8 +69,6 @@
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"superagent": "^9.0.2",
"terser": "^5.30.3",
"threads": "^1.7.0",
"tinycon": "0.6.8",
"tsx": "^4.10.5",
"ueberdb2": "^4.2.63",