Files
NodeBB/src/meta/css.js
Barış Soner Uşaklı d7a29a3df2 bs5 import
2022-08-24 10:27:15 -04:00

224 lines
6.9 KiB
JavaScript

'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', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', 'litera',
'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone',
'simplex', 'sketchy', 'slate', 'solar', 'spacelab', 'superhero', 'united',
'vapor', 'yeti', 'zephyr',
];
const buildImports = {
client: function (source, themeData) {
return [
'@import "mixins";',
'@import "generics";',
'@import "fontawesome";',
boostrapImport(themeData),
'@import "responsive-utilities";',
source,
'@import "jquery-ui";',
'@import "@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput";',
'@import "cropperjs/dist/cropper";',
'@import "flags";',
'@import "global";',
'@import "modals";',
].join('\n');
},
admin: function (source) {
return [
'@import "bootstrap/scss/bootstrap";',
'@import "mixins.scss";',
'@import "generics.scss";',
'@import "responsive-utilities.scss";',
'@import "fontawesome";',
'@import "admin/admin.scss";',
source,
'@import "jquery-ui.scss";',
'@import "@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput";',
'@import "../public/vendor/mdl/material";',
].join('\n');
},
};
function boostrapImport(themeData) {
// see https://getbootstrap.com/docs/5.0/customize/sass/#variable-defaults
// for an explanation of this order and https://bootswatch.com/help/
const { bootswatchSkin } = themeData;
return [
bootswatchSkin ? `@import "bootswatch/dist/${bootswatchSkin}/variables";` : '',
'@import "bootstrap/scss/mixins/banner";',
'@include bsBanner("");',
// functions must be included first
'@import "bootstrap/scss/functions";',
// overrides for bs5 variables
'@import "./scss/overrides";', // this file is in the themes scss folder
'@import "../public/scss/overrides.scss";', // core scss overrides
// bs files
'@import "bootstrap/scss/variables";',
'@import "bootstrap/scss/maps";',
'@import "bootstrap/scss/mixins";',
'@import "bootstrap/scss/utilities";',
// Layout & components
'@import "bootstrap/scss/root";',
'@import "bootstrap/scss/reboot";',
'@import "bootstrap/scss/type";',
'@import "bootstrap/scss/images";',
'@import "bootstrap/scss/containers";',
'@import "bootstrap/scss/grid";',
'@import "bootstrap/scss/tables";',
'@import "bootstrap/scss/forms";',
'@import "bootstrap/scss/buttons";',
'@import "bootstrap/scss/transitions";',
'@import "bootstrap/scss/dropdown";',
'@import "bootstrap/scss/button-group";',
'@import "bootstrap/scss/nav";',
'@import "bootstrap/scss/navbar";',
'@import "bootstrap/scss/card";',
'@import "bootstrap/scss/accordion";',
'@import "bootstrap/scss/breadcrumb";',
'@import "bootstrap/scss/pagination";',
'@import "bootstrap/scss/badge";',
'@import "bootstrap/scss/alert";',
'@import "bootstrap/scss/progress";',
'@import "bootstrap/scss/list-group";',
'@import "bootstrap/scss/close";',
'@import "bootstrap/scss/toasts";',
'@import "bootstrap/scss/modal";',
'@import "bootstrap/scss/tooltip";',
'@import "bootstrap/scss/popover";',
'@import "bootstrap/scss/carousel";',
'@import "bootstrap/scss/spinners";',
'@import "bootstrap/scss/offcanvas";',
'@import "bootstrap/scss/placeholders";',
// Helpers
'@import "bootstrap/scss/helpers";',
// Utilities
'@import "bootstrap/scss/utilities/api";',
// scss-docs-end import-stack
'@import "./theme";', // rest of the theme scss
bootswatchSkin ? `@import "bootswatch/dist/${bootswatchSkin}/bootswatch";` : '',
].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, extension) {
const pluginDirectories = [];
let source = '';
function pathToImport(file) {
if (!file) {
return '';
}
// trim css extension so it inlines the css like less (inline)
const parsed = path.parse(file);
const newFile = path.join(parsed.dir, parsed.name);
return `\n@import "${newFile.replace(/\\/g, '/')}";`;
}
files.forEach((styleFile) => {
if (styleFile.endsWith(extension)) {
source += pathToImport(styleFile);
} else {
pluginDirectories.push(styleFile);
}
});
await Promise.all(pluginDirectories.map(async (directory) => {
const styleFiles = await file.walk(directory);
styleFiles.forEach((styleFile) => {
source += pathToImport(styleFile);
});
}));
return source;
}
async function getBundleMetadata(target) {
const paths = [
path.join(__dirname, '../../node_modules'),
path.join(__dirname, '../../public/scss'),
path.join(__dirname, '../../public/vendor/fontawesome/scss'),
];
// Skin support
let skin;
if (target.startsWith('client-')) {
skin = target.split('-')[1];
if (CSS.supportedSkins.includes(skin)) {
target = 'client';
}
}
let themeData = null;
if (target === 'client') {
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;
}
const [scssImports, cssImports, acpScssImports] = await Promise.all([
filterGetImports(plugins.scssFiles, '.scss'),
filterGetImports(plugins.cssFiles, '.css'),
target === 'client' ? '' : filterGetImports(plugins.acpScssFiles, '.scss'),
]);
async function filterGetImports(files, prefix, extension) {
const filteredFiles = await filterMissingFiles(files);
return await getImports(filteredFiles, prefix, extension);
}
let imports = `${cssImports}\n${scssImports}\n${acpScssImports}`;
imports = buildImports[target](imports, themeData);
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;
};