You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nodebb/src/meta/minifier.js

257 lines
5.4 KiB
JavaScript

'use strict';
const fs = require('fs');
const os = require('os');
const uglify = require('uglify-es');
const async = require('async');
const winston = require('winston');
const less = require('less');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const clean = require('postcss-clean');
const fork = require('./debugFork');
require('../file'); // for graceful-fs
const Minifier = module.exports;
const pool = [];
const free = [];
let maxThreads = 0;
Object.defineProperty(Minifier, 'maxThreads', {
get: function () {
return maxThreads;
},
set: function (val) {
maxThreads = val;
if (!process.env.minifier_child) {
winston.verbose(`[minifier] utilizing a maximum of ${maxThreads} additional threads`);
}
},
configurable: true,
enumerable: true,
});
Minifier.maxThreads = os.cpus().length - 1;
Minifier.killAll = function () {
pool.forEach((child) => {
child.kill('SIGTERM');
});
pool.length = 0;
free.length = 0;
};
function getChild() {
if (free.length) {
return free.shift();
}
const proc = fork(__filename, [], {
cwd: __dirname,
env: {
minifier_child: true,
},
});
pool.push(proc);
return proc;
}
function freeChild(proc) {
proc.removeAllListeners();
free.push(proc);
}
function removeChild(proc) {
const i = pool.indexOf(proc);
if (i !== -1) {
pool.splice(i, 1);
}
}
function forkAction(action) {
return new Promise((resolve, reject) => {
const proc = getChild();
proc.on('message', (message) => {
freeChild(proc);
if (message.type === 'error') {
return reject(new Error(message.message));
}
if (message.type === 'end') {
resolve(message.result);
}
});
proc.on('error', (err) => {
proc.kill();
removeChild(proc);
reject(err);
});
proc.send({
type: 'action',
action: action,
});
});
}
const actions = {};
if (process.env.minifier_child) {
process.on('message', async (message) => {
if (message.type === 'action') {
const { action } = message;
if (typeof actions[action.act] !== 'function') {
process.send({
type: 'error',
message: 'Unknown action',
});
return;
}
try {
const result = await actions[action.act](action);
process.send({
type: 'end',
result: result,
});
} catch (err) {
process.send({
type: 'error',
message: err.stack || err.message || 'unknown error',
});
}
}
});
}
async function executeAction(action, fork) {
if (fork && (pool.length - free.length) < Minifier.maxThreads) {
return await forkAction(action);
}
if (typeof actions[action.act] !== 'function') {
throw new Error('Unknown action');
}
return await actions[action.act](action);
}
actions.concat = async function concat(data) {
if (data.files && data.files.length) {
const files = await async.mapLimit(data.files, 1000, async ref => await fs.promises.readFile(ref.srcPath, 'utf8'));
const output = files.join('\n;');
await fs.promises.writeFile(data.destPath, output);
}
};
actions.minifyJS_batch = async function minifyJS_batch(data) {
await async.eachLimit(data.files, 100, async (fileObj) => {
const source = await fs.promises.readFile(fileObj.srcPath, 'utf8');
const filesToMinify = [
{
srcPath: fileObj.srcPath,
filename: fileObj.filename,
source: source,
},
];
await minifyAndSave({
files: filesToMinify,
destPath: fileObj.destPath,
filename: fileObj.filename,
});
});
};
actions.minifyJS = async function minifyJS(data) {
const filesToMinify = await async.mapLimit(data.files, 1000, async (fileObj) => {
const source = await fs.promises.readFile(fileObj.srcPath, 'utf8');
return {
srcPath: fileObj.srcPath,
filename: fileObj.filename,
source: source,
};
});
await minifyAndSave({
files: filesToMinify,
destPath: data.destPath,
filename: data.filename,
});
};
async function minifyAndSave(data) {
const scripts = {};
data.files.forEach((ref) => {
if (ref && ref.filename && ref.source) {
scripts[ref.filename] = ref.source;
}
});
const minified = uglify.minify(scripts, {
sourceMap: {
filename: data.filename,
url: `${String(data.filename).split(/[/\\]/).pop()}.map`,
includeSources: true,
},
compress: false,
});
if (minified.error) {
throw new Error(`Error minifying ${minified.error.filename}\n${minified.error.stack}`);
}
await Promise.all([
fs.promises.writeFile(data.destPath, minified.code),
fs.promises.writeFile(`${data.destPath}.map`, minified.map),
]);
}
Minifier.js = {};
Minifier.js.bundle = async function (data, minify, fork) {
return await executeAction({
act: minify ? 'minifyJS' : 'concat',
files: data.files,
filename: data.filename,
destPath: data.destPath,
}, fork);
};
Minifier.js.minifyBatch = async function (scripts, fork) {
return await executeAction({
act: 'minifyJS_batch',
files: scripts,
}, fork);
};
actions.buildCSS = async function buildCSS(data) {
const lessOutput = await less.render(data.source, {
paths: data.paths,
javascriptEnabled: true,
});
const postcssArgs = [autoprefixer];
if (data.minify) {
postcssArgs.push(clean({
processImportFrom: ['local'],
}));
}
const result = await postcss(postcssArgs).process(lessOutput.css, {
from: undefined,
});
return { code: result.css };
};
Minifier.css = {};
Minifier.css.bundle = async function (source, paths, minify, fork) {
return await executeAction({
act: 'buildCSS',
source: source,
paths: paths,
minify: minify,
}, fork);
};
require('../promisify')(exports);