'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(); };