mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Added more performant minifiers for css and js.
This commit is contained in:
parent
3a1ef560ec
commit
eec1dd18d2
8 changed files with 87 additions and 112 deletions
|
@ -6,12 +6,6 @@
|
||||||
"shutdown": "ep_etherpad-lite/node/db/DB"
|
"shutdown": "ep_etherpad-lite/node/db/DB"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Minify",
|
|
||||||
"hooks": {
|
|
||||||
"shutdown": "ep_etherpad-lite/node/utils/Minify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "express",
|
"name": "express",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|
|
@ -28,6 +28,8 @@ let sessionStore: { shutdown: () => void; } | null;
|
||||||
const sockets:Set<Socket> = new Set();
|
const sockets:Set<Socket> = new Set();
|
||||||
const socketsEvents = new events.EventEmitter();
|
const socketsEvents = new events.EventEmitter();
|
||||||
const startTime = stats.settableGauge('httpStartTime');
|
const startTime = stats.settableGauge('httpStartTime');
|
||||||
|
import https from 'https';
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
exports.server = null;
|
exports.server = null;
|
||||||
|
|
||||||
|
@ -119,11 +121,8 @@ exports.restartServer = async () => {
|
||||||
options.ca.push(fs.readFileSync(caFileName));
|
options.ca.push(fs.readFileSync(caFileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const https = require('https');
|
|
||||||
exports.server = https.createServer(options, app);
|
exports.server = https.createServer(options, app);
|
||||||
} else {
|
} else {
|
||||||
const http = require('http');
|
|
||||||
exports.server = http.createServer(app);
|
exports.server = http.createServer(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import {MapArrayType} from "../../types/MapType";
|
||||||
import {PartType} from "../../types/PartType";
|
import {PartType} from "../../types/PartType";
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const minify = require('../../utils/Minify');
|
import {requestURIs, minify} from '../../utils/Minify';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
import CachingMiddleware from '../../utils/caching_middleware';
|
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
|
// Minify will serve static files compressed (minify enabled). It also has
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
// 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
|
// Setup middleware that will package JavaScript files served by minify for
|
||||||
// CommonJS loader on the client-side.
|
// CommonJS loader on the client-side.
|
||||||
|
@ -51,7 +51,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
rootURI: 'http://invalid.invalid/static/js/',
|
rootURI: 'http://invalid.invalid/static/js/',
|
||||||
libraryPath: 'javascripts/lib/',
|
libraryPath: 'javascripts/lib/',
|
||||||
libraryURI: 'http://invalid.invalid/static/plugins/',
|
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;
|
const StaticAssociator = Yajsml.associators.StaticAssociator;
|
||||||
|
|
|
@ -31,11 +31,13 @@ import axios from "axios";
|
||||||
|
|
||||||
const settings = require('./utils/Settings');
|
const settings = require('./utils/Settings');
|
||||||
|
|
||||||
|
import wtfnodeMod from 'wtfnode';
|
||||||
|
|
||||||
let wtfnode: any;
|
let wtfnode: any;
|
||||||
if (settings.dumpOnUncleanExit) {
|
if (settings.dumpOnUncleanExit) {
|
||||||
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and
|
// 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.
|
// it should be above everything else so that it can hook in before resources are used.
|
||||||
wtfnode = require('wtfnode');
|
wtfnode = wtfnodeMod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,21 +21,22 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {compressCSS, compressJS} from "./MinifyWorker";
|
||||||
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
const fs = require('fs').promises;
|
import {promises as fs} from 'fs'
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||||
const RequireKernel = require('etherpad-require-kernel');
|
const RequireKernel = require('etherpad-require-kernel');
|
||||||
const mime = require('mime-types');
|
import mime from 'mime-types';
|
||||||
const Threads = require('threads');
|
import log4js from 'log4js';
|
||||||
const log4js = require('log4js');
|
|
||||||
const sanitizePathname = require('./sanitizePathname');
|
const sanitizePathname = require('./sanitizePathname');
|
||||||
|
|
||||||
const logger = log4js.getLogger('Minify');
|
const logger = log4js.getLogger('Minify');
|
||||||
|
|
||||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||||
|
|
||||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
|
||||||
|
|
||||||
const LIBRARY_WHITELIST = [
|
const LIBRARY_WHITELIST = [
|
||||||
'async',
|
'async',
|
||||||
|
@ -49,10 +50,10 @@ const LIBRARY_WHITELIST = [
|
||||||
|
|
||||||
// What follows is a terrible hack to avoid loop-back within the server.
|
// 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.
|
// 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);
|
const parsedUrl = new URL(url);
|
||||||
let status = 500;
|
let status = 500;
|
||||||
const content = [];
|
const content: string[] = [];
|
||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
|
@ -62,7 +63,7 @@ const requestURI = async (url, method, headers) => {
|
||||||
let mockResponse;
|
let mockResponse;
|
||||||
const p = new Promise((resolve) => {
|
const p = new Promise((resolve) => {
|
||||||
mockResponse = {
|
mockResponse = {
|
||||||
writeHead: (_status, _headers) => {
|
writeHead: (_status: number, _headers: MapArrayType<any>) => {
|
||||||
status = _status;
|
status = _status;
|
||||||
for (const header in _headers) {
|
for (const header in _headers) {
|
||||||
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
|
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();
|
headers[header.toLowerCase()] = value.toString();
|
||||||
},
|
},
|
||||||
header: (header, value) => {
|
header: (header: string, value: string) => {
|
||||||
headers[header.toLowerCase()] = value.toString();
|
headers[header.toLowerCase()] = value.toString();
|
||||||
},
|
},
|
||||||
write: (_content) => {
|
write: (_content: string) => {
|
||||||
_content && content.push(_content);
|
_content && content.push(_content);
|
||||||
},
|
},
|
||||||
end: (_content) => {
|
end: (_content: string) => {
|
||||||
_content && content.push(_content);
|
_content && content.push(_content);
|
||||||
resolve([status, headers, content.join('')]);
|
resolve([status, headers, content.join('')]);
|
||||||
},
|
},
|
||||||
|
@ -89,16 +90,17 @@ const requestURI = async (url, method, headers) => {
|
||||||
return await p;
|
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) => {
|
Promise.all(locations.map(async (loc) => {
|
||||||
try {
|
try {
|
||||||
return await requestURI(loc, method, headers);
|
return await requestURI(loc, method, headers);
|
||||||
} catch (err) {
|
} catch (err:any) {
|
||||||
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
||||||
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
||||||
return [500, headers, ''];
|
return [500, headers, ''];
|
||||||
}
|
}
|
||||||
})).then((responses) => {
|
// @ts-ignore
|
||||||
|
})).then((responses:[[number,number,number]]) => {
|
||||||
const statuss = responses.map((x) => x[0]);
|
const statuss = responses.map((x) => x[0]);
|
||||||
const headerss = responses.map((x) => x[1]);
|
const headerss = responses.map((x) => x[1]);
|
||||||
const contentss = responses.map((x) => x[2]);
|
const contentss = responses.map((x) => x[2]);
|
||||||
|
@ -120,11 +122,11 @@ const compatPaths = {
|
||||||
* @param req the Express request
|
* @param req the Express request
|
||||||
* @param res the Express response
|
* @param res the Express response
|
||||||
*/
|
*/
|
||||||
const minify = async (req, res) => {
|
export const minify = async (req: any, res:any) => {
|
||||||
let filename = req.params.filename;
|
let filename = req.params.filename;
|
||||||
try {
|
try {
|
||||||
filename = sanitizePathname(filename);
|
filename = sanitizePathname(filename);
|
||||||
} catch (err) {
|
} catch (err:any) {
|
||||||
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
||||||
res.writeHead(404, {});
|
res.writeHead(404, {});
|
||||||
res.end();
|
res.end();
|
||||||
|
@ -132,6 +134,7 @@ const minify = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility for plugins that require() files from old paths.
|
// Backward compatibility for plugins that require() files from old paths.
|
||||||
|
// @ts-ignore
|
||||||
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
||||||
if (newLocation != null) {
|
if (newLocation != null) {
|
||||||
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
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) || [];
|
const [, testf] = /^plugins\/ep_etherpad-lite\/(tests\/frontend\/.*)/.exec(filename) || [];
|
||||||
if (testf != null) filename = `../${testf}`;
|
if (testf != null) filename = `../${testf}`;
|
||||||
|
|
||||||
const contentType = mime.lookup(filename);
|
const contentType = mime.lookup(filename) as string;
|
||||||
|
|
||||||
const [date, exists] = await statFile(filename, 3);
|
const [date, exists] = await statFile(filename, 3);
|
||||||
if (date) {
|
if (date) {
|
||||||
|
@ -186,7 +189,7 @@ const minify = async (req, res) => {
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
res.writeHead(404, {});
|
res.writeHead(404, {});
|
||||||
res.end();
|
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.writeHead(304, {});
|
||||||
res.end();
|
res.end();
|
||||||
} else if (req.method === 'HEAD') {
|
} 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.
|
// 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
|
* The only external call to this function provides an explicit value for
|
||||||
* dirStatLimit: this check could be removed.
|
* dirStatLimit: this check could be removed.
|
||||||
|
@ -227,9 +230,10 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
let stats;
|
let stats;
|
||||||
try {
|
try {
|
||||||
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
||||||
} catch (err) {
|
} catch (err:any) {
|
||||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
||||||
// Stat the directory instead.
|
// Stat the directory instead.
|
||||||
|
// @ts-ignore
|
||||||
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
||||||
return [date, false];
|
return [date, false];
|
||||||
}
|
}
|
||||||
|
@ -241,7 +245,7 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
|
|
||||||
const lastModifiedDateOfEverything = async () => {
|
const lastModifiedDateOfEverything = async () => {
|
||||||
const folders2check = [path.join(ROOT_DIR, 'js/'), path.join(ROOT_DIR, 'css/')];
|
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
|
// go through this two folders
|
||||||
await Promise.all(folders2check.map(async (dir) => {
|
await Promise.all(folders2check.map(async (dir) => {
|
||||||
// read the files in the folder
|
// read the files in the folder
|
||||||
|
@ -251,7 +255,7 @@ const lastModifiedDateOfEverything = async () => {
|
||||||
files.push('.');
|
files.push('.');
|
||||||
|
|
||||||
// go through all files in this folder
|
// 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
|
// get the stat data of this file
|
||||||
const stats = await fs.stat(path.join(dir, filename));
|
const stats = await fs.stat(path.join(dir, filename));
|
||||||
|
|
||||||
|
@ -269,58 +273,39 @@ const lastModifiedDateOfEverything = async () => {
|
||||||
const _requireLastModified = new Date();
|
const _requireLastModified = new Date();
|
||||||
const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`;
|
const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`;
|
||||||
|
|
||||||
const getFileCompressed = async (filename, contentType) => {
|
const getFileCompressed = async (filename: string, contentType: string) => {
|
||||||
let content = await getFile(filename);
|
let content = await getFile(filename);
|
||||||
if (!content || !settings.minify) {
|
if (!content || !settings.minify) {
|
||||||
return content;
|
return content;
|
||||||
} else if (contentType === 'application/javascript') {
|
} else if (contentType === 'application/javascript') {
|
||||||
return await new Promise((resolve) => {
|
try {
|
||||||
threadsPool.queue(async ({compressJS}) => {
|
logger.info('Compress JS file %s.', filename);
|
||||||
try {
|
|
||||||
logger.info('Compress JS file %s.', filename);
|
|
||||||
|
|
||||||
content = content.toString();
|
content = content.toString();
|
||||||
const compressResult = await compressJS(content);
|
const compressResult = compressJS(content);
|
||||||
|
|
||||||
if (compressResult.error) {
|
content = compressResult.code.toString(); // Convert content obj code to string
|
||||||
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
|
} catch (error) {
|
||||||
} else {
|
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}`);
|
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
||||||
}
|
}
|
||||||
resolve(content);
|
return content
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (contentType === 'text/css') {
|
} else if (contentType === 'text/css') {
|
||||||
return await new Promise((resolve) => {
|
try {
|
||||||
threadsPool.queue(async ({compressCSS}) => {
|
logger.info('Compress CSS file %s.', filename);
|
||||||
try {
|
content = compressCSS(filename, ROOT_DIR, Buffer.from(content)).toString();
|
||||||
logger.info('Compress CSS file %s.', filename);
|
return content
|
||||||
|
} catch (error) {
|
||||||
content = await compressCSS(filename, ROOT_DIR);
|
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||||
} catch (error) {
|
}
|
||||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
|
||||||
}
|
|
||||||
resolve(content);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFile = async (filename) => {
|
const getFile = async (filename: string) => {
|
||||||
if (filename === 'js/require-kernel.js') return requireDefinition();
|
if (filename === 'js/require-kernel.js') return requireDefinition();
|
||||||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
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.minify = (req: Request, res: Response, next: Function) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||||
|
|
||||||
exports.requestURIs = requestURIs;
|
|
||||||
|
|
||||||
exports.shutdown = async (hookName, context) => {
|
|
||||||
await threadsPool.terminate();
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
});
|
|
29
src/node/utils/MinifyWorker.ts
Normal file
29
src/node/utils/MinifyWorker.ts
Normal 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()
|
||||||
|
}
|
||||||
|
};
|
|
@ -30,9 +30,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@swc/core": "^1.4.8",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"clean-css": "^5.3.3",
|
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
"jsonminify": "0.4.2",
|
"jsonminify": "0.4.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"languages4translatewiki": "0.1.3",
|
"languages4translatewiki": "0.1.3",
|
||||||
|
"lightningcss": "^1.24.1",
|
||||||
"live-plugin-manager": "^1.0.0",
|
"live-plugin-manager": "^1.0.0",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
|
@ -68,8 +69,6 @@
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"superagent": "^9.0.2",
|
"superagent": "^9.0.2",
|
||||||
"terser": "^5.30.3",
|
|
||||||
"threads": "^1.7.0",
|
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"tsx": "^4.10.5",
|
"tsx": "^4.10.5",
|
||||||
"ueberdb2": "^4.2.63",
|
"ueberdb2": "^4.2.63",
|
||||||
|
|
Loading…
Reference in a new issue