'use strict'; const winston = require('winston'); const nconf = require('nconf'); const fs = require('fs'); const util = require('util'); const path = require('path'); const rimraf = require('rimraf'); const rimrafAsync = util.promisify(rimraf); const plugins = require('../plugins'); const db = require('../database'); const file = require('../file'); const minifier = require('./minifier'); const CSS = module.exports; CSS.supportedSkins = [ 'cerulean', 'cyborg', 'flatly', 'journal', 'lumen', 'paper', 'simplex', 'spacelab', 'united', 'cosmo', 'darkly', 'readable', 'sandstone', 'slate', 'superhero', 'yeti', ]; const buildImports = { client: function (source) { return `@import "./theme";\n${source}\n${[ '@import "../public/vendor/fontawesome/less/regular.less";', '@import "../public/vendor/fontawesome/less/solid.less";', '@import "../public/vendor/fontawesome/less/brands.less";', '@import "../public/vendor/fontawesome/less/fontawesome.less";', '@import "../public/vendor/fontawesome/less/v4-shims.less";', '@import "../public/vendor/fontawesome/less/nodebb-shims.less";', '@import "../../public/less/jquery-ui.less";', '@import (inline) "../node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.css";', '@import (inline) "../node_modules/cropperjs/dist/cropper.css";', '@import "../../public/less/flags.less";', '@import "../../public/less/generics.less";', '@import "../../public/less/mixins.less";', '@import "../../public/less/global.less";', '@import "../../public/less/modals.less";', ].map(str => str.replace(/\//g, path.sep)).join('\n')}`; }, admin: function (source) { return `${source}\n${[ '@import "../public/vendor/fontawesome/less/regular.less";', '@import "../public/vendor/fontawesome/less/solid.less";', '@import "../public/vendor/fontawesome/less/brands.less";', '@import "../public/vendor/fontawesome/less/fontawesome.less";', '@import "../public/vendor/fontawesome/less/v4-shims.less";', '@import "../public/vendor/fontawesome/less/nodebb-shims.less";', '@import "../public/less/admin/admin";', '@import "../public/less/generics.less";', '@import "../../public/less/jquery-ui.less";', '@import (inline) "../node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.css";', '@import (inline) "../public/vendor/mdl/material.css";', ].map(str => str.replace(/\//g, path.sep)).join('\n')}`; }, }; async function filterMissingFiles(filepaths) { const exists = await Promise.all( filepaths.map(async (filepath) => { const exists = await file.exists(path.join(__dirname, '../../node_modules', filepath)); if (!exists) { winston.warn(`[meta/css] File not found! ${filepath}`); } return exists; }) ); return filepaths.filter((filePath, i) => exists[i]); } async function getImports(files, prefix, extension) { const pluginDirectories = []; let source = ''; files.forEach((styleFile) => { if (styleFile.endsWith(extension)) { source += `${prefix + path.sep + styleFile}";`; } else { pluginDirectories.push(styleFile); } }); await Promise.all(pluginDirectories.map(async (directory) => { const styleFiles = await file.walk(directory); styleFiles.forEach((styleFile) => { source += `${prefix + path.sep + styleFile}";`; }); })); return source; } async function getBundleMetadata(target) { const paths = [ path.join(__dirname, '../../node_modules'), path.join(__dirname, '../../public/less'), path.join(__dirname, '../../public/vendor/fontawesome/less'), ]; // Skin support let skin; if (target.startsWith('client-')) { skin = target.split('-')[1]; if (CSS.supportedSkins.includes(skin)) { target = 'client'; } } let skinImport = []; if (target === 'client') { const themeData = await db.getObjectFields('config', ['theme:type', 'theme:id', 'bootswatchSkin']); const themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); const baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); paths.unshift(baseThemePath); themeData.bootswatchSkin = skin || themeData.bootswatchSkin; if (themeData && themeData.bootswatchSkin) { skinImport.push(`\n@import "./@nodebb/bootswatch/${themeData.bootswatchSkin}/variables.less";`); skinImport.push(`\n@import "./@nodebb/bootswatch/${themeData.bootswatchSkin}/bootswatch.less";`); } skinImport = skinImport.join(''); } const [lessImports, cssImports, acpLessImports] = await Promise.all([ filterGetImports(plugins.lessFiles, '\n@import ".', '.less'), filterGetImports(plugins.cssFiles, '\n@import (inline) ".', '.css'), target === 'client' ? '' : filterGetImports(plugins.acpLessFiles, '\n@import ".', '.less'), ]); async function filterGetImports(files, prefix, extension) { const filteredFiles = await filterMissingFiles(files); return await getImports(filteredFiles, prefix, extension); } let imports = `${skinImport}\n${cssImports}\n${lessImports}\n${acpLessImports}`; imports = buildImports[target](imports); return { paths: paths, imports: imports }; } CSS.buildBundle = async function (target, fork) { if (target === 'client') { await rimrafAsync(path.join(__dirname, '../../build/public/client*')); } const data = await getBundleMetadata(target); const minify = process.env.NODE_ENV !== 'development'; const bundle = await minifier.css.bundle(data.imports, data.paths, minify, fork); const filename = `${target}.css`; await fs.promises.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code); return bundle.code; };