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.

352 lines
9.7 KiB
JavaScript

'use strict';
const path = require('path');
const fs = require('fs');
const util = require('util');
let mkdirp = require('mkdirp');
// TODO: remove in 1.16.0
if (!mkdirp.hasOwnProperty('native')) {
mkdirp = util.promisify(mkdirp);
}
const rimraf = require('rimraf');
const rimrafAsync = util.promisify(rimraf);
const file = require('../file');
const plugins = require('../plugins');
const minifier = require('./minifier');
const JS = module.exports;
JS.scripts = {
base: [
'node_modules/socket.io-client/dist/socket.io.js',
'node_modules/requirejs/require.js',
'public/src/require-config.js',
'node_modules/jquery/dist/jquery.js',
'node_modules/textcomplete/dist/textcomplete.min.js',
'node_modules/textcomplete.contenteditable/dist/textcomplete.codemirror.min.js',
'node_modules/visibilityjs/lib/visibility.core.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.js',
'node_modules/benchpressjs/build/benchpress.js',
'node_modules/jquery-serializeobject/jquery.serializeObject.js',
'node_modules/jquery-deserialize/src/jquery.deserialize.js',
'public/vendor/bootbox/wrapper.js',
'public/src/utils.js',
'public/src/sockets.js',
'public/src/app.js',
'public/src/ajaxify.js',
'public/src/overrides.js',
'public/src/widgets.js',
],
// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
rjs: [
'public/src/client/header/chat.js',
'public/src/client/header/notifications.js',
'public/src/client/infinitescroll.js',
'public/src/client/pagination.js',
'public/src/client/recent.js',
'public/src/client/unread.js',
'public/src/client/topic.js',
'public/src/client/topic/events.js',
'public/src/client/topic/posts.js',
'public/src/client/topic/images.js',
'public/src/client/topic/votes.js',
'public/src/client/topic/postTools.js',
'public/src/client/topic/threadTools.js',
'public/src/client/categories.js',
'public/src/client/category.js',
'public/src/client/category/tools.js',
'public/src/modules/translator.js',
'public/src/modules/components.js',
'public/src/modules/hooks.js',
'public/src/modules/sort.js',
'public/src/modules/navigator.js',
'public/src/modules/topicSelect.js',
'public/src/modules/topicList.js',
'public/src/modules/categoryFilter.js',
'public/src/modules/categorySelector.js',
'public/src/modules/categorySearch.js',
'public/src/modules/share.js',
'public/src/modules/alerts.js',
'public/src/modules/taskbar.js',
'public/src/modules/helpers.js',
'public/src/modules/storage.js',
'public/src/modules/handleBack.js',
],
admin: [
'node_modules/material-design-lite/material.js',
'public/src/admin/admin.js',
'node_modules/jquery-deserialize/src/jquery.deserialize.js',
],
// modules listed below are built (/src/modules) so they can be defined anonymously
modules: {
'Chart.js': 'node_modules/chart.js/dist/Chart.min.js',
'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js',
'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js',
'jquery-ui': 'node_modules/jquery-ui/ui',
'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
ace: 'node_modules/ace-builds/src-min',
'clipboard.js': 'node_modules/clipboard/dist/clipboard.min.js',
'tinycon.js': 'node_modules/tinycon/tinycon.js',
'slideout.js': 'node_modules/slideout/dist/slideout.min.js',
'compare-versions.js': 'node_modules/compare-versions/index.js',
'timeago/locales': 'node_modules/timeago/locales',
'jquery-form.js': 'node_modules/jquery-form/dist/jquery.form.min.js',
'xregexp.js': 'node_modules/xregexp/xregexp-all.js',
},
};
async function linkIfLinux(srcPath, destPath) {
if (process.platform === 'win32') {
await fs.promises.copyFile(srcPath, destPath);
} else {
await file.link(srcPath, destPath, true);
}
}
const basePath = path.resolve(__dirname, '../..');
async function minifyModules(modules, fork) {
const moduleDirs = modules.reduce((prev, mod) => {
const dir = path.resolve(path.dirname(mod.destPath));
if (!prev.includes(dir)) {
prev.push(dir);
}
return prev;
}, []);
await Promise.all(moduleDirs.map(dir => mkdirp(dir)));
const filtered = modules.reduce((prev, mod) => {
if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
prev.skip.push(mod);
} else {
prev.minify.push(mod);
}
return prev;
}, { minify: [], skip: [] });
await Promise.all([
minifier.js.minifyBatch(filtered.minify, fork),
...filtered.skip.map(mod => linkIfLinux(mod.srcPath, mod.destPath)),
]);
}
async function linkModules() {
const { modules } = JS.scripts;
await Promise.all(Object.keys(modules).map(async (relPath) => {
const srcPath = path.join(__dirname, '../../', modules[relPath]);
const destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
const [stats] = await Promise.all([
fs.promises.stat(srcPath),
mkdirp(path.dirname(destPath)),
]);
if (stats.isDirectory()) {
await file.linkDirs(srcPath, destPath, true);
return;
}
await linkIfLinux(srcPath, destPath);
}));
}
const moduleDirs = ['modules', 'admin', 'client'];
async function getModuleList() {
let modules = Object.keys(JS.scripts.modules).map(relPath => ({
srcPath: path.join(__dirname, '../../', JS.scripts.modules[relPath]),
destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
}));
const coreDirs = moduleDirs.map(dir => ({
srcPath: path.join(__dirname, '../../public/src', dir),
destPath: path.join(__dirname, '../../build/public/src', dir),
}));
modules = modules.concat(coreDirs);
const moduleFiles = [];
await Promise.all(modules.map(async (module) => {
const { srcPath } = module;
const { destPath } = module;
const stats = await fs.promises.stat(srcPath);
if (!stats.isDirectory()) {
moduleFiles.push(module);
return;
}
const files = await file.walk(srcPath);
const mods = files.filter(
filePath => path.extname(filePath) === '.js'
).map(filePath => ({
srcPath: path.normalize(filePath),
destPath: path.join(destPath, path.relative(srcPath, filePath)),
}));
moduleFiles.push(...mods);
}));
moduleFiles.forEach((mod) => {
mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
});
return moduleFiles;
}
async function clearModules() {
const builtPaths = moduleDirs.map(
p => path.join(__dirname, '../../build/public/src', p)
);
await Promise.all(
builtPaths.map(builtPath => rimrafAsync(builtPath))
);
}
JS.buildModules = async function (fork) {
await clearModules();
if (process.env.NODE_ENV === 'development') {
await linkModules();
return;
}
const modules = await getModuleList();
await minifyModules(modules, fork);
};
async function requirejsOptimize(target) {
const requirejs = require('requirejs');
let scriptText = '';
const sharedCfg = {
paths: {
jquery: 'empty:',
},
optimize: 'none',
out: function (text) {
scriptText += text;
},
};
const bundledModules = [
{
baseUrl: path.join(basePath, 'node_modules'),
name: 'timeago/jquery.timeago',
},
{
baseUrl: path.join(basePath, 'node_modules/nprogress'),
name: 'nprogress',
},
{
baseUrl: path.join(basePath, 'node_modules/bootbox'),
name: 'bootbox',
},
];
const targetModules = {
admin: [
{
baseUrl: path.join(basePath, 'node_modules/sortablejs'),
name: 'Sortable',
},
],
client: [],
};
const optimizeAsync = util.promisify((config, cb) => {
requirejs.optimize(config, () => cb(), err => cb(err));
});
const allModules = bundledModules.concat(targetModules[target]);
for (const moduleCfg of allModules) {
// eslint-disable-next-line no-await-in-loop
await optimizeAsync({ ...sharedCfg, ...moduleCfg });
}
const filePath = path.join(__dirname, `../../build/public/rjs-bundle-${target}.js`);
await fs.promises.writeFile(filePath, scriptText);
}
JS.linkStatics = async function () {
await rimrafAsync(path.join(__dirname, '../../build/public/plugins'));
await Promise.all(Object.keys(plugins.staticDirs).map(async (mappedPath) => {
const sourceDir = plugins.staticDirs[mappedPath];
const destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
await mkdirp(path.dirname(destDir));
await file.linkDirs(sourceDir, destDir, true);
}));
};
async function getBundleScriptList(target) {
const pluginDirectories = [];
if (target === 'admin') {
target = 'acp';
}
let pluginScripts = plugins[`${target}Scripts`].filter((path) => {
if (path.endsWith('.js')) {
return true;
}
pluginDirectories.push(path);
return false;
});
await Promise.all(pluginDirectories.map(async (directory) => {
const scripts = await file.walk(directory);
pluginScripts = pluginScripts.concat(scripts);
}));
let scripts = JS.scripts.base;
if (target === 'client') {
scripts = scripts.concat(JS.scripts.rjs);
} else if (target === 'acp') {
scripts = scripts.concat(JS.scripts.admin);
}
scripts = scripts.concat(pluginScripts).map((script) => {
const srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
return {
srcPath: srcPath,
filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
};
});
return scripts;
}
JS.buildBundle = async function (target, fork) {
const fileNames = {
client: 'nodebb.min.js',
admin: 'acp.min.js',
};
await requirejsOptimize(target);
const files = await getBundleScriptList(target);
files.push({
srcPath: path.join(__dirname, `../../build/public/rjs-bundle-${target}.js`),
});
const minify = process.env.NODE_ENV !== 'development';
const filePath = path.join(__dirname, '../../build/public', fileNames[target]);
await minifier.js.bundle({
files: files,
filename: fileNames[target],
destPath: filePath,
}, minify, fork);
};
JS.killMinifier = function () {
minifier.killAll();
};