reldens
Version:
Reldens - MMORPG Platform
735 lines (690 loc) • 27.1 kB
JavaScript
/**
*
* Reldens - ThemeManager
*
* Manages theme assets, paths, and bundling for the Reldens game project.
* Handles theme installation, asset copying, CSS/JS bundling with Parcel,
* and provides path resolution for project files, theme files, and Reldens module files.
* Coordinates between the project root, theme directory, dist directory, and node_modules.
*
*/
const { TemplatesList } = require('../../admin/server/templates-list');
const { TemplateEngine } = require('./template-engine');
const { TemplatesToPathMapper } = require('./templates-to-path-mapper');
const { GameConst } = require('../constants');
const { Parcel, createWorkerFarm } = require('@parcel/core');
const { FileHandler } = require('@reldens/server-utils');
const { ErrorManager, Logger, sc } = require('@reldens/utils');
/**
* @typedef {Object} ThemeManagerProps
* @property {string} projectRoot
* @property {string} [projectThemeName]
* @property {boolean} [jsSourceMaps]
* @property {boolean} [cssSourceMaps]
*/
class ThemeManager
{
/** @type {string} */
projectRoot = '';
/** @type {string} */
projectRootPackageJson = '';
/** @type {string} */
envFilePath = '';
/** @type {string} */
gitignoreFilePath = '';
/** @type {string} */
installationLockPath = '';
/** @type {string} */
reldensModulePath = '';
/** @type {string} */
reldensModuleLibPath = '';
/** @type {string} */
reldensModuleThemePath = '';
/** @type {string} */
reldensModuleDefaultThemePath = '';
/** @type {string} */
reldensModuleDefaultThemeAssetsPath = '';
/** @type {string} */
reldensModuleThemePluginsPath = '';
/** @type {string} */
reldensModuleInstallerPath = '';
/** @type {string} */
reldensModulePathInstallTemplatesFolder = '';
/** @type {string} */
reldensModuleThemeAdminPath = '';
/** @type {string} */
distPath = '';
/** @type {string} */
assetsDistPath = '';
/** @type {string} */
cssDistPath = '';
/** @type {string} */
themePath = '';
/** @type {string} */
projectThemeName = GameConst.STRUCTURE.DEFAULT;
/** @type {string} */
projectThemePath = '';
/** @type {string} */
projectPluginsPath = '';
/** @type {string} */
projectAdminPath = '';
/** @type {string} */
projectAssetsPath = '';
/** @type {string} */
projectCssPath = '';
/** @type {string} */
projectIndexPath = '';
/** @type {string} */
projectGenerateDataPath = '';
/** @type {string} */
projectGeneratedDataPath = '';
/** @type {Object<string, any>} */
defaultBrowserBundleOptions = {};
/**
* @param {ThemeManagerProps} props
*/
constructor(props)
{
if(!sc.hasOwn(props, 'projectRoot')){
ErrorManager.error('Missing project property.');
}
/** @type {string} */
this.encoding = (process.env.RELDENS_DEFAULT_ENCODING || 'utf8');
/** @type {TemplateEngine} */
this.templateEngine = TemplateEngine;
/** @type {TemplatesList} */
this.adminTemplatesList = TemplatesList;
/** @type {boolean} */
this.jsSourceMaps = sc.get(props, 'jsSourceMaps', false);
/** @type {boolean} */
this.cssSourceMaps = sc.get(props, 'cssSourceMaps', false);
this.setupPaths(props);
}
/**
* @param {ThemeManagerProps} props
*/
setupPaths(props)
{
let structure = GameConst.STRUCTURE;
this.projectRoot = sc.get(props, 'projectRoot', '');
this.projectRootPackageJson = FileHandler.joinPaths(this.projectRoot, 'package.json');
this.envFilePath = FileHandler.joinPaths(this.projectRoot, '.env');
this.gitignoreFilePath = FileHandler.joinPaths(this.projectRoot, '.gitignore');
this.installationLockPath = FileHandler.joinPaths(this.projectRoot, structure.INSTALL_LOCK);
this.projectThemeName = sc.get(props, 'projectThemeName', structure.DEFAULT);
this.projectGenerateDataPath = FileHandler.joinPaths(this.projectRoot, 'generate-data');
this.projectGeneratedDataPath = FileHandler.joinPaths(this.projectGenerateDataPath, 'generated');
this.reldensModulePath = sc.get(
props,
'reldensModulePath',
FileHandler.joinPaths(this.projectRoot, 'node_modules', 'reldens')
);
this.reldensModuleLibPath = FileHandler.joinPaths(this.reldensModulePath, structure.LIB);
this.reldensModuleThemePath = FileHandler.joinPaths(this.reldensModulePath, structure.THEME);
this.reldensModuleDefaultThemePath = FileHandler.joinPaths(this.reldensModuleThemePath, structure.DEFAULT);
this.reldensModuleDefaultThemeAssetsPath = FileHandler.joinPaths(
this.reldensModuleDefaultThemePath,
structure.ASSETS
);
this.reldensModuleThemePluginsPath = FileHandler.joinPaths(this.reldensModuleThemePath, structure.PLUGINS);
this.reldensModuleThemeAdminPath = FileHandler.joinPaths(this.reldensModuleThemePath, structure.ADMIN);
this.reldensModuleInstallerPath = FileHandler.joinPaths(this.reldensModulePath, structure.INSTALLER_FOLDER);
this.reldensModuleInstallerIndexPath = FileHandler.joinPaths(this.reldensModuleInstallerPath, structure.INDEX);
this.reldensModulePathInstallTemplatesFolder = FileHandler.joinPaths(
this.reldensModulePath,
structure.LIB,
'game',
structure.SERVER,
'install-templates'
);
this.reldensModulePathInstallTemplateEnvDist = FileHandler.joinPaths(
this.reldensModulePathInstallTemplatesFolder,
'.env.dist'
);
this.reldensModulePathInstallTemplateGitignoreDist = FileHandler.joinPaths(
this.reldensModulePathInstallTemplatesFolder,
'.gitignore.dist'
);
this.reldensModulePathInstallTemplateKnexDist = FileHandler.joinPaths(
this.reldensModulePathInstallTemplatesFolder,
'knexfile.js.dist'
);
this.installerPath = FileHandler.joinPaths(this.projectRoot, structure.INSTALLER_FOLDER);
this.installerPathIndex = FileHandler.joinPaths(this.installerPath, structure.INDEX);
this.themePath = FileHandler.joinPaths(this.projectRoot, structure.THEME);
this.projectAdminPath = FileHandler.joinPaths(this.themePath, structure.ADMIN);
this.projectAdminTemplatesPath = FileHandler.joinPaths(this.projectAdminPath, structure.TEMPLATES);
this.adminTemplates = TemplatesToPathMapper.map(this.adminTemplatesList, this.projectAdminTemplatesPath);
this.distPath = FileHandler.joinPaths(this.projectRoot, structure.DIST);
this.assetsDistPath = FileHandler.joinPaths(this.distPath, structure.ASSETS);
this.cssDistPath = FileHandler.joinPaths(this.distPath, structure.CSS);
this.projectThemePath = FileHandler.joinPaths(this.themePath, this.projectThemeName);
this.projectPluginsPath = FileHandler.joinPaths(this.themePath, structure.PLUGINS);
this.projectAssetsPath = FileHandler.joinPaths(this.projectThemePath, structure.ASSETS);
this.projectCssPath = FileHandler.joinPaths(this.projectThemePath, structure.CSS);
this.projectIndexPath = FileHandler.joinPaths(this.projectThemePath, structure.INDEX);
}
/**
* @returns {Object<string, string>}
*/
paths()
{
return {
projectRoot: this.projectRoot,
reldensModulePath: this.reldensModulePath,
reldensModuleLibPath: this.reldensModuleLibPath,
reldensModuleThemePath: this.reldensModuleThemePath,
reldensModuleDefaultThemePath: this.reldensModuleDefaultThemePath,
reldensModuleDefaultThemeAssetsPath: this.reldensModuleDefaultThemeAssetsPath,
reldensModuleThemePluginsPath: this.reldensModuleThemePluginsPath,
distPath: this.distPath,
assetsDistPath: this.assetsDistPath,
themePath: this.themePath,
projectThemePath: this.projectThemePath,
projectPluginsPath: this.projectPluginsPath,
projectAssetsPath: this.projectAssetsPath,
projectIndexPath: this.projectIndexPath
};
}
/**
* @param {...string} args
* @returns {string}
*/
assetPath(...args)
{
return FileHandler.joinPaths(this.projectAssetsPath, ...args);
}
/**
* @returns {boolean}
*/
permissionsCheck()
{
return FileHandler.permissionsCheck(this.projectRoot);
}
resetDist()
{
this.removeDist();
FileHandler.createFolder(this.distPath);
FileHandler.createFolder(this.assetsDistPath);
FileHandler.createFolder(this.cssDistPath);
Logger.info('Reset "dist" folder, created: '+this.distPath);
}
/**
* @returns {boolean}
*/
removeDist()
{
return FileHandler.remove(this.distPath);
}
installDefaultTheme()
{
if(!FileHandler.copyFolderSync(this.reldensModuleDefaultThemePath, this.projectThemePath)){
Logger.error('File copy folder sync error.', FileHandler.error);
}
if(!FileHandler.copyFolderSync(this.reldensModuleThemePluginsPath, this.projectPluginsPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
}
if(!FileHandler.copyFolderSync(this.reldensModuleThemeAdminPath, this.projectAdminPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
}
if(!FileHandler.copyFolderSync(this.reldensModuleDefaultThemePath, this.distPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
}
Logger.info('Install "default" theme:'
+'\n'+this.reldensModuleDefaultThemePath+' > '+this.projectThemePath
+'\n'+this.reldensModuleThemePluginsPath+' > '+this.projectPluginsPath
+'\n'+this.reldensModuleDefaultThemePath+' > '+this.distPath
);
}
/**
* @returns {boolean}
*/
copyAssetsToDist()
{
if(!FileHandler.copyFolderSync(this.projectAssetsPath, this.assetsDistPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
Logger.info('Copied "assets" to "dist" from:' +'\n'+this.projectAssetsPath+' > '+this.assetsDistPath);
return true;
}
copyKnexFile()
{
let knexFile = FileHandler.joinPaths(this.projectRoot, 'knexfile.js');
FileHandler.copyFileSyncIfDoesNotExist(
FileHandler.joinPaths(this.reldensModulePathInstallTemplatesFolder, 'knexfile.js.dist'),
knexFile
);
Logger.info('Reminder: edit the knexfile.js file!');
}
copyEnvFile()
{
FileHandler.copyFileSyncIfDoesNotExist(
FileHandler.joinPaths(this.reldensModulePathInstallTemplatesFolder, '.env.dist'),
this.envFilePath
);
Logger.info('Reminder: edit the .env file!');
}
copyGitignoreFile()
{
FileHandler.copyFileSyncIfDoesNotExist(
FileHandler.joinPaths(this.reldensModulePathInstallTemplatesFolder, '.gitignore.dist'),
this.gitignoreFilePath
);
Logger.info('Reminder: edit the .gitignore file!');
}
/**
* @param {boolean} [override=false]
* @returns {Promise<boolean>}
*/
async copyIndex(override = false)
{
let indexFile = FileHandler.joinPaths(this.projectRoot, 'index.js');
if(FileHandler.exists(indexFile) && !override){
Logger.info('File already exists: index.js');
return false;
}
let templatePath = FileHandler.joinPaths(this.reldensModuleThemePath, 'index.js.dist');
let fileContent = FileHandler.fetchFileContents(templatePath);
if(!fileContent){
Logger.error('Failed to read template file.', templatePath);
return false;
}
let parsedContents = await this.templateEngine.render(
fileContent,
{yourThemeName: this.projectThemeName || 'default'}
);
try {
await FileHandler.updateFileContents(indexFile, parsedContents.toString());
} catch (error) {
Logger.error('Failed to create index.js file.', error);
}
}
/**
* @returns {boolean}
*/
copyDefaultAssets()
{
if(!FileHandler.copyFolderSync(this.reldensModuleDefaultThemeAssetsPath, this.assetsDistPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
Logger.info('Copied default assets:'+'\n'+this.reldensModuleDefaultThemeAssetsPath+' > '+this.assetsDistPath);
return true;
}
/**
* @returns {boolean}
*/
copyDefaultTheme()
{
if(!FileHandler.copyFolderSync(this.reldensModuleDefaultThemePath, this.projectThemePath)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
Logger.info('Copied default theme:'+'\n'+this.reldensModuleDefaultThemePath+' > '+this.projectThemePath);
return true;
}
/**
* @returns {boolean}
*/
copyPackage()
{
if(!FileHandler.copyFolderSync(this.reldensModuleThemePluginsPath, this.projectPluginsPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
Logger.info('Copied plugins:'+'\n'+this.reldensModuleThemePluginsPath+' > '+this.projectPluginsPath);
return true;
}
/**
* @returns {boolean}
*/
copyAdmin()
{
if(!FileHandler.copyFolderSync(this.reldensModuleThemeAdminPath, this.projectAdminPath)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
Logger.info('Copied admin:'+'\n'+this.reldensModuleThemeAdminPath+' > '+this.projectAdminPath);
return true;
}
/**
* @returns {Promise<boolean|void>}
*/
async buildCss()
{
let allowBuildCss = 1 === Number(sc.get(process.env, 'RELDENS_ALLOW_BUILD_CSS', 1));
if(!allowBuildCss){
Logger.info('CSS build skipped (RELDENS_ALLOW_BUILD_CSS=0)');
return false;
}
let themeScss = FileHandler.joinPaths(this.projectCssPath, GameConst.STRUCTURE.SCSS_FILE).toString();
let bundler = this.createCssBundler(themeScss);
try {
let { buildTime } = await bundler.run();
Logger.info('Built Game CSS in '+buildTime+'ms!');
} catch (error) {
Logger.critical({'Parcel diagnostics for error': sc.get(error, 'diagnostics', error)});
ErrorManager.error('Parcel build CSS process failed.');
}
FileHandler.createFolder(this.cssDistPath);
FileHandler.copyFileSyncIfDoesNotExist(
FileHandler.joinPaths(this.projectCssPath, GameConst.STRUCTURE.CSS_FILE),
FileHandler.joinPaths(this.cssDistPath, GameConst.STRUCTURE.CSS_FILE)
);
}
copyAdminFiles()
{
let jsDistPath = FileHandler.joinPaths(this.distPath, 'js');
FileHandler.createFolder(jsDistPath);
FileHandler.createFolder(this.cssDistPath);
// copy functions.js to dist/js:
let functionsSource = FileHandler.joinPaths(this.projectAdminPath, 'functions.js');
FileHandler.copyFile(functionsSource, FileHandler.joinPaths(jsDistPath, 'functions.js'));
// copy reldens-specific functions to dist root:
let reldensFunctionsSource = FileHandler.joinPaths(this.projectAdminPath, 'reldens-functions.js');
FileHandler.copyFile(reldensFunctionsSource, FileHandler.joinPaths(this.distPath, 'reldens-functions.js'));
// copy admin js to dist root:
let adminJsSource = FileHandler.joinPaths(this.projectAdminPath, GameConst.STRUCTURE.ADMIN_JS_FILE);
FileHandler.copyFile(adminJsSource, FileHandler.joinPaths(this.distPath, GameConst.STRUCTURE.ADMIN_JS_FILE));
// copy admin css to dist/css:
let adminCssSource = FileHandler.joinPaths(this.projectAdminPath, GameConst.STRUCTURE.ADMIN_CSS_FILE);
FileHandler.copyFile(adminCssSource, FileHandler.joinPaths(this.cssDistPath, GameConst.STRUCTURE.ADMIN_CSS_FILE));
Logger.info('Admin files copied to dist.');
}
/**
* @returns {Promise<boolean>}
*/
async copyAdminAssetsToDist()
{
if(!FileHandler.copyFolderSync(
FileHandler.joinPaths(this.projectAdminPath, GameConst.STRUCTURE.ASSETS),
this.assetsDistPath
)){
Logger.error('File copy folder sync error.', FileHandler.error);
return false;
}
return true;
}
/**
* @returns {Promise<void>}
*/
async buildSkeleton()
{
await this.buildCss();
await this.buildClient();
Logger.info('Built Skeleton.');
}
/**
* @returns {Promise<boolean|void>}
*/
async buildClient()
{
let allowBuildClient = 1 === Number(sc.get(process.env, 'RELDENS_ALLOW_BUILD_CLIENT', 1));
if(!allowBuildClient){
Logger.info('Client build skipped (RELDENS_ALLOW_BUILD_CLIENT=0)');
return false;
}
let elementsCollection = FileHandler.readFolder(this.projectThemePath);
for(let element of elementsCollection){
if(-1 === element.indexOf('.html')){
continue;
}
let elementPath = FileHandler.joinPaths(this.projectThemePath, element);
if(!FileHandler.isFile(elementPath)){
continue;
}
try {
let bundler = this.createBrowserBundler(elementPath);
let { buildTime } = await bundler.run();
Logger.info('Built '+elementPath+' in '+buildTime+'ms!');
} catch (error) {
Logger.critical({'Parcel diagnostics for error': sc.get(error, 'diagnostics', error), elementPath});
ErrorManager.error('Parcel build Game Client process failed.');
}
}
}
/**
* @param {string} folderPath
* @returns {Promise<void>}
*/
async clearBundlerCache(folderPath)
{
FileHandler.remove(FileHandler.joinPaths(folderPath, '.parcel-cache'));
}
/**
* @returns {Promise<void>}
*/
async buildInstaller()
{
try {
let bundleOptions = this.generateDefaultBrowserBundleOptions(this.reldensModuleInstallerIndexPath);
FileHandler.createFolder(this.installerPath);
bundleOptions.targets.modern.distDir = this.installerPath;
bundleOptions.defaultTargetOptions.distDir = this.installerPath;
let bundler = new Parcel(bundleOptions);
let { buildTime } = await bundler.run();
Logger.info('Built '+this.reldensModuleInstallerIndexPath+' in '+buildTime+'ms!');
} catch (error) {
Logger.critical('Parcel diagnostics for error on build installer.', sc.get(error, 'diagnostics', error));
ErrorManager.error('Parcel build installer process failed.');
}
}
/**
* @param {string} entryPath
* @returns {Parcel}
*/
createBrowserBundler(entryPath)
{
return new Parcel(this.generateDefaultBrowserBundleOptions(entryPath));
}
/**
* @param {string} entryPath
* @returns {Object<string, any>}
*/
generateDefaultBrowserBundleOptions(entryPath)
{
let workerFarm = createWorkerFarm({ backend: 'process' });
this.defaultBrowserBundleOptions = {
defaultConfig: 'reldens/lib/bundlers/drivers/parcel-config',
shouldDisableCache: true,
workerFarm,
targets: {
modern: {
engines: {
browsers: ['> 0.5%, last 2 versions, not dead']
},
distDir: this.distPath,
outputFormat: 'esmodule'
},
},
entries: entryPath,
logLevel: 'verbose',
defaultTargetOptions: {
shouldDisableCache: true,
shouldOptimize: true,
sourceMaps: this.jsSourceMaps,
distEntry: entryPath,
distDir: this.distPath,
isLibrary: false,
outputFormat: 'esmodule'
}
};
return this.defaultBrowserBundleOptions;
}
/**
* @param {string} entryPath
* @returns {Parcel}
*/
createCssBundler(entryPath)
{
let workerFarm = createWorkerFarm({ backend: 'process' });
return new Parcel({
defaultConfig: 'reldens/lib/bundlers/drivers/parcel-config',
entries: entryPath,
shouldDisableCache: true,
workerFarm,
defaultTargetOptions: {
shouldDisableCache: true,
shouldOptimize: true,
sourceMaps: this.cssSourceMaps,
distDir: this.projectCssPath,
isLibrary: false,
outputFormat: 'esmodule',
publicUrl: './'
}
});
}
copyNew()
{
this.copyDefaultAssets();
this.copyDefaultTheme();
this.copyPackage();
this.copyAdmin();
}
/**
* @returns {Promise<void>}
*/
async fullRebuild()
{
this.copyNew();
await this.buildSkeleton();
this.copyAdminFiles();
}
/**
* @returns {Promise<void>}
*/
async installSkeleton()
{
await this.copyIndex(true);
await this.copyServerFiles();
this.resetDist();
await this.fullRebuild();
}
/**
* @returns {Promise<void>}
*/
async createApp()
{
await this.copyIndex(true);
await this.updatePackageJson();
this.validateOrCreateTheme();
this.resetDist();
await this.fullRebuild();
}
/**
* @returns {Promise<void>}
*/
async copyServerFiles()
{
this.copyEnvFile();
this.copyKnexFile();
this.copyGitignoreFile();
await this.copyIndex();
}
/**
* @returns {boolean}
*/
distPathExists()
{
let result = FileHandler.exists(this.distPath);
Logger.info('Dist path: '+this.distPath, 'Dist folder exists? '+(result ? 'yes' : 'no'));
return result;
}
/**
* @returns {boolean}
*/
themePathExists()
{
let result = FileHandler.exists(this.projectThemePath);
Logger.info('Theme path: '+this.projectThemePath, 'Theme folder exists? '+(result ? 'yes' : 'no'));
return result;
}
validateOrCreateTheme()
{
let distExists = this.distPathExists();
let themeExists = this.themePathExists();
if(false === themeExists){
this.installDefaultTheme();
Logger.error(
'Project theme folder was not found: '+this.projectThemeName
+'\nA copy from default has been made.'
);
}
if(false === distExists){
this.copyAssetsToDist();
}
}
/**
* @param {string} filePath
* @param {Object<string, any>} [params]
* @returns {Promise<string|false>}
*/
async loadAndRenderTemplate(filePath, params)
{
if(!FileHandler.exists(filePath)){
Logger.error('Template not found.', {filePath});
return false;
}
let fileContent = FileHandler.fetchFileContents(filePath);
return await this.templateEngine.render(fileContent, params);
}
/**
* @returns {Promise<boolean|void>}
*/
async createClientBundle()
{
// @TODO - BETA - Remove this function, just move to an auto-installation on first run feature.
let runBundler = 1 === Number(sc.get(process.env, 'RELDENS_ALLOW_RUN_BUNDLER', 0));
if(!runBundler){
return false;
}
let forceResetDistOnBundle = 1 === Number(sc.get(process.env, 'RELDENS_FORCE_RESET_DIST_ON_BUNDLE', 0));
if(forceResetDistOnBundle){
await this.resetDist();
}
let forceCopyAssetsOnBundle = 1 === Number(sc.get(process.env, 'RELDENS_FORCE_COPY_ASSETS_ON_BUNDLE', 0));
if(forceCopyAssetsOnBundle){
this.copyAssetsToDist();
}
Logger.info('Running bundle on: '+this.projectIndexPath);
await this.buildClient();
}
/**
* @returns {Promise<void>}
*/
async updatePackageJson()
{
let jsonFile = FileHandler.joinPaths(this.projectRoot, 'package.json');
Logger.info('Updating package.json.', jsonFile);
let readFile = jsonFile;
if(!FileHandler.exists(jsonFile)){
readFile = FileHandler.joinPaths(this.reldensModulePathInstallTemplatesFolder, 'data-package.json');
Logger.error('File package.json does not exists, changing read-file.', jsonFile);
}
let data = FileHandler.fetchFileJson(readFile);
if(!data){
Logger.critical('Invalid package.json data.', readFile);
return;
}
if(!data.alias){
data.alias = {};
}
if(data.alias.process){
Logger.critical('Data alias process already exists, it must be set to "false" for the bundler.', data);
data.alias.processBackup = data.alias.process;
}
data.alias.process = false;
if(!data.targets){
data.targets = {};
}
if(data.targets.main){
Logger.critical('Data targets main already exists, it must be set to "false" for the bundler.', data);
data.targets.mainBackup = data.targets.main;
}
data.targets.main = false;
await FileHandler.updateFileContents(jsonFile, JSON.stringify(data, null, 2));
Logger.info('File package.json updated successfully.', jsonFile);
}
}
module.exports.ThemeManager = ThemeManager;