nw-skeleton

Source: app-wrapper/js/helper/system/staticFilesHelper.js

/**
 * @fileOverview StaticFilesHelper class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const postcss = require('postcss');
const postcssUrl = require('postcss-url');
const AppBaseClass = require('../../lib/appBase').AppBaseClass;

var _appWrapper;
var appState;

/**
 * StaticFilesHelper class - handles and manages static (js and css) file operations
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberof appWrapper.helpers.systemHelpers
 * @property {Object}   jsFileLoadResolves    Object containing promises for added js files (each file resolves promise on load)
 * @property {Object}   cssFileLoadResolves   Object containing promises for added css files (each file resolves promise on load)
 * @property {string[]} watchedFiles          Array of watched file paths
 */
class StaticFilesHelper extends AppBaseClass {

    /**
     * Creates StaticFilesHelper instance
     *
     * @constructor
     * @return {StaticFilesHelper}              Instance of StaticFilesHelper class
     */
    constructor() {
        super();

        _appWrapper = window.getAppWrapper();
        appState = _appWrapper.getAppState();

        this.jsFileLoadResolves = {};
        this.cssFileLoadResolves = {};

        this.boundMethods = {
            cssFileChanged: null,
            doReloadCss: null,
        };

        this.timeouts = {
            reloadCss: null
        };

        this.watchedFiles = [];

        return this;
    }

    /**
     * Postcss-url [custom rebase function]{@link https://github.com/postcss/postcss-url#url-function}
     *
     * @param  {Object} asset Asset data
     * @param  {Object} dir   Directories data
     * @return {string}       Absolute path to asset
     */
    rebaseAsset(asset, dir){
        _.noop(dir);
        if (asset.url && asset.url.match(/^#/)){
            return asset.url;
        } else {
            return 'file://' + asset.absolutePath.replace(/\\/g, '/');
        }
    }

    /**
     * Loads css file, returning its contents
     *
     * @async
     * @param  {string}     href    Relative or absolute path to css file
     * @param  {Boolean}    noWatch Flag that prevents file watching
     * @return {string}             Css file contents
     */
    async loadCss (href, noWatch) {
        let cssFilePath = href;
        let cssContents = '';
        let compiledCssPath = this.getCompiledCssPath();


        if (!await _appWrapper.fileManager.isFile(cssFilePath)){
            cssFilePath  = path.resolve(path.join('.' + href));
        }
        if (await _appWrapper.fileManager.isFile(cssFilePath)){

            cssContents = await _appWrapper.fileManager.loadFile(cssFilePath);

            if (cssContents){
                cssContents = await postcss().use(postcssUrl({url: this.rebaseAsset})).process(cssContents, { from: cssFilePath, to: compiledCssPath });
            }

            if (!noWatch && this.getConfig('liveCss') && this.getConfig('debug.enabled')){
                this.watchedFiles.push(cssFilePath);
                _appWrapper.fileManager.watch(cssFilePath, {persistent: false}, this.boundMethods.cssFileChanged);
            }
        } else {
            this.log('Problem loading CSS file "{1}" - file does not exist', 'error', [cssFilePath]);
        }
        return cssContents;
    }

    /**
     * Add css files to current page head tag
     *
     * @async
     * @param {string}  href    Path to css file
     * @param {Boolean} noWatch Flag that prevents file watching
     * @param {Boolean} silent  Flag that prevents logging
     * @return {undefined}
     */
    async addCss (href, noWatch, silent) {
        let utilHelper = _appWrapper.getHelper('util');
        let headEl = document.querySelector('head');
        let linkHref = href;

        let processDir = process.cwd();
        let processDirRegex = new RegExp('^' + utilHelper.quoteRegex(processDir));
        if (href.match(processDirRegex)){
            linkHref = href.replace(processDirRegex, '');
        } else if (href.match(/^\./)){
            href = path.resolve(href);
            linkHref = href.replace(processDirRegex, '');
        } else {
            if (await _appWrapper.fileManager.isFile(href)){
                linkHref = 'file://' + href;
            } else if (await _appWrapper.fileManager.isFile(path.join(processDir, href))){
                href = path.join(processDir, href);
            } else {
                this.log('Possible CSS file problem for "{1}"', 'warning', href);
            }
        }

        if (!noWatch && this.getConfig('liveCss') && this.getConfig('debug.enabled')){
            this.watchedFiles.push(href);
            _appWrapper.fileManager.watch(href, {persistent: false}, this.boundMethods.cssFileChanged);
        }

        if (!headEl){
            this.log('No <head> element found for adding css file!', 'error');
            return false;
        } else {
            if (!silent){
                this.log('Adding CSS file "{1}"...', 'debug', [href]);
            }
            var returnPromise = new Promise((resolve) => {
                this.cssFileLoadResolves[href] = resolve;
            });
            let cssNode = document.createElement('link');

            cssNode.onload = () => {
                cssNode.onload = null;
                this.cssFileLoadResolves[href](true);

                // this.cssFileLoadResolves[href] = null;
            };

            cssNode.setAttribute('rel', 'stylesheet');
            cssNode.setAttribute('type', 'text/css');
            cssNode.setAttribute('href', linkHref);

            headEl.appendChild(cssNode);
        }
        return returnPromise;
    }

    /**
     * Refreshes css loaded on page by adding random query selector to file url
     *
     * @async
     * @return {undefined}
     */
    async refreshCss () {
        var links = window.document.querySelectorAll('link');
        if (links && links.length){
            this.refreshLinkTags(links);
        } else {
            this.log('No CSS files to reload.', 'warning', []);
        }
    }

    /**
     * Refreshses css files upon css file change on disk
     *
     * @async
     * @param  {string[]}   changedFiles  Array of changed css file paths
     * @return {undefined}
     */
    async refreshCssFiles (changedFiles) {
        let processDir = process.cwd();
        let processDirRegex = new RegExp('^' + processDir);
        let changedHrefs = _.map(changedFiles, (filePath) => {
            return filePath.replace(processDirRegex, '');
        });

        let linkSelectors = [];
        for(let i=0;i<changedHrefs.length;i++){
            linkSelectors.push('link[href*="' + changedHrefs[i] + '"]');
        }

        let links = window.document.querySelectorAll(linkSelectors.join(', '));

        if (links && links.length){
            this.refreshLinkTags(links);
        } else {
            this.log('No CSS files to reload (1).', 'warning', []);
        }
    }

    /**
     * Refreshes selected <link> tags on page by adding (or replacing) random query string to their urls
     *
     * @param  {DOMElement[]} links Array of <link> elements to refresh
     * @return {undefined}
     */
    refreshLinkTags(links){
        if (_.isUndefined(links)){
            links = document.querySelectorAll('link');
        }
        this.log('Reloading {1} CSS files.', 'group', [links.length]);
        let headEl = document.querySelector('head');
        let linkCount = links.length;
        let loadedLinks = 0;
        let newLinks = [];
        for (let i=0; i<links.length; i++) {
            if (links[i].type && links[i].type == 'text/css'){
                this.log('Reloading CSS file "{1}"', 'info', [links[i].href.replace(/^[^/]+\/\/[^/]+/, '').replace(/\?.*$/, '')]);
                let newHref = links[i].href + '';
                newLinks[i] = document.createElement('link');

                newLinks[i].onload = async (e) => {
                    let newLink = e.target;
                    newLink.onload = null;
                    loadedLinks++;
                    this.log('Reloaded CSS file "{1}"', 'info', [newLink.href.replace(/^[^/]+\/\/[^/]+/, '').replace(/\?.*$/, '')]);
                    if (loadedLinks >= linkCount){
                        await this.removeOldCssTags(links);
                        this.log('Reloading {1} CSS files.', 'groupend', [links.length]);
                    }
                };

                newLinks[i].setAttribute('rel', 'stylesheet');
                newLinks[i].setAttribute('type', 'text/css');
                newLinks[i].setAttribute('href', newHref);
                // newLinks[i].setAttribute('data-new', 'true');
                headEl.appendChild(newLinks[i]);
            }
        }
    }

    /**
     * Removes old CSS tags from head element
     *
     * @async
     * @param  {DOMElement[]} links Array of <link> elements to remove
     * @return {undefined}
     */
    async removeOldCssTags (links) {
        await _appWrapper.wait(1);
        this.log('Removing old CSS tags', 'group', []);
        if (_.isUndefined(links)){
            links = document.querySelectorAll('link');
        }
        for (let i=0; i<links.length;i++){
            this.log('Removing old CSS file "{1}" tag', 'info', [links[i].href]);
            links[i].parentNode.removeChild(links[i]);
        }
        this.detectMissingVariables();
        this.log('Removing old CSS tags', 'groupend', []);

    }

    /**
     * Prepares and loads css files using appState and config data
     *
     * @async
     * @param  {Boolean} silent Flag that prevents logging
     * @return {undefined}
     */
    async loadCssFiles(silent) {
        this.log('Preparing css files...', 'group', []);
        await this.generateCss(false, silent);
        let cssFile = this.getCompiledCssPath(true);
        if (this.getConfig('compileCss')){
            let result = await this.addCss(cssFile, true, silent);
            if (!result){
                this.log('Problem preparing css file "{1}"', 'error', [cssFile]);
            }
            this.cssFileLoadResolves[cssFile] = null;
            delete this.cssFileLoadResolves[cssFile];
        }
        this.detectMissingVariables();
        this.log('Preparing css files...', 'groupend', []);
    }

    /**
     * Generates css by compiling all files or adding all files to head tag based on configuration
     *
     * @async
     * @param {Boolean} noWatch Flag that prevents file watching
     * @param {Boolean} silent  Flag that prevents logging
     * @return {undefined}
     */
    async generateCss(noWatch, silent) {
        if (this.getConfig('compileCss')){
            let compiledCss = await this.compileCss(noWatch, silent);
            if (compiledCss) {
                // let compiledCssPath = path.resolve(path.join('.', this.getConfig('appConfig.cssCompiledFile')));
                let compiledCssPath = this.getCompiledCssPath();
                await this.writeCss(compiledCssPath, compiledCss);
            }
        } else {
            var cssFileData = await this.getCssFileData();
            if (cssFileData.counts.totalCssFileCount){
                if (!silent){
                    this.log('Adding {1} CSS files', 'group', [cssFileData.counts.totalCssFileCount]);
                }

                let basicTypes = [
                    'initCssFiles',
                    'themeInitCssFiles',
                    'appCssFiles',
                    'themeCssFiles',
                ];

                let debugTypes = [
                    'debugCssFiles',
                    'appDebugCssFiles',
                ];

                let overrideTypes = [
                    'componentCssFiles',
                    'themeOverrideCssFiles',
                ];

                for (let j=0;j<basicTypes.length;j++){
                    await this.addCssFiles(cssFileData.files[basicTypes[j]], noWatch, silent);
                }

                if (appState.isDebugWindow){
                    for (let j=0;j<debugTypes.length;j++){
                        await this.addCssFiles(cssFileData.files[debugTypes[j]], noWatch, silent);
                    }
                }

                for (let j=0;j<overrideTypes.length;j++){
                    await this.addCssFiles(cssFileData.files[overrideTypes[j]], noWatch, silent);
                }

                if (!silent){
                    this.log('Adding {1} CSS files', 'groupend', [cssFileData.counts.totalCssFileCount]);
                }
            }
        }
    }

    /**
     * Adds css files to head
     *
     * @async
     * @param {string[]}    cssFiles An array of css files to add
     * @param {Boolean}     noWatch  Flag to prevent filesystem watching of compiled files
     * @param {Boolean}     silent   Flag to prevent logging output
     * @return {undefined}
     */
    async addCssFiles(cssFiles, noWatch, silent){
        if (cssFiles && cssFiles.length){
            for (let i=0; i<cssFiles.length; i++){
                await this.addCss(cssFiles[i], noWatch, silent);
            }
        }
    }

    /**
     * Writes css contents to file from argument
     *
     * @async
     * @param  {string} filePath    Absolute css file path
     * @param  {string} cssContents CSS contents to write
     * @return {undefined}
     */
    async writeCss(filePath, cssContents){
        await _appWrapper.fileManager.createDirRecursive(path.dirname(filePath));
        fs.writeFileSync(filePath, cssContents, {flag: 'w'});
    }

    /**
     * Returns prepared css data object, getting info from appState and configuration
     *
     * @async
     * @return {Object} Css files data object
     */
    async getCssFileData () {
        let themeHelper = _appWrapper.getHelper('theme');

        let initCssFiles = this.getConfig('appConfig.initCssFiles') || [];
        let appCssFiles = this.getConfig('appConfig.cssFiles') || [];
        let debugCssFiles = this.getConfig('appConfig.debugCssFiles') || [];
        let appDebugCssFiles = this.getConfig('appConfig.appDebugCssFiles') || [];
        let componentCssFiles = appState.componentCssFiles || [];

        initCssFiles = _.uniq(_.compact(initCssFiles));
        appCssFiles = _.uniq(_.compact(appCssFiles));
        debugCssFiles = _.uniq(_.compact(debugCssFiles));
        appDebugCssFiles = _.uniq(_.compact(appDebugCssFiles));
        componentCssFiles = _.uniq(_.compact(componentCssFiles));

        let themeInitCssFiles = [];
        let themeCssFiles = [];
        let themeOverrideCssFiles = [];

        let totalCssFileCount = 0;

        let initCssFileCount = 0;
        let appCssFileCount = 0;
        let debugCssFileCount = 0;
        let appDebugCssFileCount = 0;
        let componentCssFileCount = 0;

        let themeInitCssFileCount = 0;
        let themeCssFileCount = 0;
        let themeOverrideCssFileCount = 0;

        let themeName = this.getConfig('theme');
        let themeConfig;
        if (themeName){
            themeConfig = await themeHelper.getThemeConfig(themeName);
            if (!(themeConfig && themeConfig.name)){
                this.addUserMessage('Problem loading theme "{1}", resorting to "basic" theme!', 'error', [themeName]);
                themeName = 'basic';
                appState.config.theme = themeName;
                themeConfig = await themeHelper.getThemeConfig(themeName);
            }
            if (themeConfig && themeConfig.name){
                themeInitCssFiles = await themeHelper.getThemeTypeCssFiles(themeConfig, 'initCssFiles');
                themeInitCssFileCount += themeInitCssFiles.length;
                totalCssFileCount += themeInitCssFiles.length;

                themeCssFiles = await themeHelper.getThemeTypeCssFiles(themeConfig, 'cssFiles');
                themeCssFileCount += themeCssFiles.length;
                totalCssFileCount += themeCssFiles.length;

                themeOverrideCssFiles = await themeHelper.getThemeTypeCssFiles(themeConfig, 'overrideCssFiles');
                themeOverrideCssFileCount += themeOverrideCssFiles.length;
                totalCssFileCount += themeOverrideCssFiles.length;
            }
        }


        if (initCssFiles && initCssFiles.length){
            initCssFileCount = initCssFiles.length;
        }
        if (appCssFiles && appCssFiles.length){
            appCssFileCount = appCssFiles.length;
        }

        if (debugCssFiles && debugCssFiles.length){
            debugCssFileCount = debugCssFiles.length;
        }

        if (appDebugCssFiles && appDebugCssFiles.length){
            appDebugCssFileCount = appDebugCssFiles.length;
        }

        if (componentCssFiles && componentCssFiles.length){
            componentCssFileCount = componentCssFiles.length;
        }

        totalCssFileCount += initCssFileCount + appCssFileCount + componentCssFileCount;

        if (appState.isDebugWindow){
            totalCssFileCount += debugCssFileCount + appDebugCssFileCount;
        }

        let cssFileData = {
            files: {
                initCssFiles,
                appCssFiles,
                debugCssFiles,
                appDebugCssFiles,
                componentCssFiles,
                themeInitCssFiles,
                themeCssFiles,
                themeOverrideCssFiles
            },
            counts: {
                initCssFileCount,
                appCssFileCount,
                debugCssFileCount,
                appDebugCssFileCount,
                componentCssFileCount,
                themeInitCssFileCount,
                themeCssFileCount,
                themeOverrideCssFileCount,
                totalCssFileCount
            }
        };

        return cssFileData;
    }

    /**
     * Returns list of css files to be loaded, using appState and configuration
     *
     * @async
     * @return {string[]} An array of CSS file paths
     */
    async getCssFiles () {
        let cssFiles = [];
        let fileData = await this.getCssFileData();
        if (fileData && fileData.files){
            for(let fileGroup in fileData.files){
                for (let i=0; i<fileData.files[fileGroup].length; i++){
                    cssFiles.push(fileData.files[fileGroup][i]);
                }
            }
        }
        return cssFiles;
    }

    /**
     * Loads css from all files in group and returns compiled css value
     *
     * @async
     * @param  {string[]}   cssFiles An array of css file paths to load
     * @param  {string}     type     Type of css file paths to load
     * @param  {Boolean}    noWatch  Flag to prevent filesystem watching of compiled files
     * @param  {Boolean}    silent   Flag to prevent logging output
     * @return {string}              Compiled css contents of all files
     */
    async compileCssTypeGroup (cssFiles, type, noWatch, silent){
        if (!type){
            this.log('No type passed for CSS group compilation.', 'error', []);
            return false;
        }
        let compiledCss = '';
        let cssFileCount = cssFiles.length;

        if (cssFileCount){
            if (!silent){
                this.log('Compiling {1} {2} CSS files', 'group', [cssFileCount, type]);
            }
            for (let i=0; i<cssFileCount; i++){
                let cssResult = await this.loadCss(cssFiles[i], noWatch);
                if (cssResult && cssResult.css){
                    let cssContents = cssResult.css;
                    compiledCss += cssContents;
                }
            }
            if (!silent){
                this.log('Compiling {1} {2} CSS files', 'groupend', [cssFileCount, type]);
            }
        }
        return compiledCss;
    }

    /**
     * Compiles all css file contents to minified css, based on configuration
     *
     * @async
     * @param {Boolean} noWatch Flag that prevents file watching
     * @param {Boolean} silent  Flag that prevents logging
     * @return {string}         Minified and compiled CSS contents
     */
    async compileCss (noWatch, silent) {
        let compiledCss = '';
        let cssFileData = await this.getCssFileData();

        if (cssFileData.counts.totalCssFileCount){
            if (!silent){
                this.log('Compiling {1} CSS files', 'group', [cssFileData.counts.totalCssFileCount]);
            }

            compiledCss += await this.compileCssTypeGroup(cssFileData.files.initCssFiles, 'init', noWatch, silent);
            compiledCss += await this.compileCssTypeGroup(cssFileData.files.themeInitCssFiles, 'theme init', noWatch, silent);
            compiledCss += await this.compileCssTypeGroup(cssFileData.files.appCssFiles, 'app', noWatch, silent);
            compiledCss += await this.compileCssTypeGroup(cssFileData.files.themeCssFiles, 'theme', noWatch, silent);

            if (appState.isDebugWindow){
                compiledCss += await this.compileCssTypeGroup(cssFileData.files.debugCssFiles, 'app-debug', noWatch, silent);
                compiledCss += await this.compileCssTypeGroup(cssFileData.files.appDebugCssFiles, 'debug', noWatch, silent);
            }

            compiledCss += await this.compileCssTypeGroup(cssFileData.files.componentCssFiles, 'component', noWatch, silent);
            compiledCss += await this.compileCssTypeGroup(cssFileData.files.themeOverrideCssFiles, 'theme override', noWatch, silent);

            if (!silent){
                this.log('{1} CSS files compiled, total size: {2}', 'info', [cssFileData.counts.totalCssFileCount, _appWrapper.getHelper('format').formatFileSize(compiledCss.length)]);
                this.log('Compiling {1} CSS files', 'groupend', [cssFileData.counts.totalCssFileCount]);
            }
        }
        return compiledCss;
    }

    /**
     * Adds js file script tag to document head element
     *
     * @async
     * @param  {string} jsFile Path to js file
     * @return {Boolean}     Js file loading result
     */
    async loadJs (jsFile) {
        let headEl = document.querySelector('head');
        let fileUrl = jsFile;
        if (!(jsFile.match(/^\//) || jsFile.match(/^file:\/\//))){
            fileUrl = '../js/' + jsFile;
        }
        if (!headEl){
            this.log('No <head> element found for adding js file!', 'error');
            return false;
        } else {
            var returnPromise = new Promise((resolve) => {
                this.jsFileLoadResolves[jsFile] = resolve;
            });
            this.log('Adding JS file "{1}"...', 'debug', [jsFile]);
            let jsNode = document.createElement('script');
            jsNode.setAttribute('type', 'text/javascript');
            jsNode.setAttribute('src', fileUrl);
            jsNode.onload = () => {
                jsNode.onload = null;
                this.jsFileLoadResolves[jsFile](true);
            };
            headEl.appendChild(jsNode);
            return returnPromise;
        }
    }

    /**
     * Loads group of js files of given type
     *
     * @async
     * @param  {string[]} jsFiles   An array of js files to load
     * @param  {string} type        Type of js files to load
     * @return {undefined}
     */
    async loadJsTypeGroup (jsFiles, type){
        if (!type){
            this.log('No type passed for JS group loading.', 'error', []);
            return false;
        }
        let jsFileCount = jsFiles.length;
        if (jsFileCount){
            this.log('Loading {1} {2} JS files', 'group', [jsFileCount, type]);
            for (let i=0; i<jsFileCount; i++){
                let result = await this.loadJs(jsFiles[i]);
                if (!result){
                    this.log('Problem loading JS file "{1}"', 'error', [jsFiles[i]]);
                }
                this.jsFileLoadResolves[jsFiles[i]] = null;
                delete this.jsFileLoadResolves[jsFiles[i]];
            }
            this.log('Loading {1} {2} JS files', 'groupend', [jsFileCount, type]);
        }
    }

    /**
     * Loads all js files using configuration
     *
     * @async
     * @return {undefined}
     */
    async loadJsFiles() {
        let themeHelper = _appWrapper.getHelper('theme');
        let jsFiles = this.getConfig('appConfig.initJsFiles');
        let appJsFiles = this.getConfig('appConfig.jsFiles');
        let themeInitJsFiles = [];
        let themeJsFiles = [];

        let totalJsFileCount = 0;

        let themeName = this.getConfig('theme');
        if (themeName){
            let themeConfig = await _appWrapper.getHelper('theme').getThemeConfig(themeName);
            if (themeConfig && themeConfig.name){
                themeInitJsFiles = await themeHelper.getThemeTypeJsFiles(themeConfig, 'initJsFiles');
                themeJsFiles = await themeHelper.getThemeTypeJsFiles(themeConfig, 'jsFiles');
            }
        }

        if (jsFiles && jsFiles.length){
            totalJsFileCount += jsFiles.length;
        }

        if (appJsFiles && appJsFiles.length){
            totalJsFileCount += appJsFiles.length;
        }

        if (themeInitJsFiles && themeInitJsFiles.length){
            totalJsFileCount += themeInitJsFiles.length;
        }

        if (themeJsFiles && themeJsFiles.length){
            totalJsFileCount += themeJsFiles.length;
        }

        if (totalJsFileCount){
            this.log('Loading {1} JS files', 'group', [totalJsFileCount]);
            await this.loadJsTypeGroup(jsFiles, 'wrapper');
            await this.loadJsTypeGroup(themeInitJsFiles, 'theme init');
            await this.loadJsTypeGroup(appJsFiles, 'app');
            await this.loadJsTypeGroup(themeJsFiles, 'theme');
            this.log('Loading {1} JS files', 'groupend', [totalJsFileCount]);
        }
    }

    /**
     * Handler that gets triggered when css file is changed on disk
     *
     * @async
     * @param  {Event}  e           Event that triggered the method
     * @param  {string} filename    Path to changed file
     * @return {undefined}
     */
    async cssFileChanged (e, filename) {
        this.log('Css file "{1}" fired event "{2}"', 'debug', [filename, e]);
        if (this.getConfig('compileCss')){
            await this.doReloadCss();
        } else {
            let filenameRegex = new RegExp(filename);
            let cssFiles = await this.getCssFiles();
            let changedFiles = [];
            for(let i=0; i<cssFiles.length;i++){
                if (cssFiles[i].match(filenameRegex)){
                    changedFiles.push(cssFiles[i]);
                }
            }
            if (changedFiles && changedFiles.length){
                await this.refreshCssFiles(changedFiles);
            }
        }
    }

    /**
     * Handler that reloads all css on page
     *
     * @param  {Event}  e           Event that triggered the method
     * @return {undefined}
     */
    reloadCss (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        clearTimeout(this.timeouts.reloadCss);
        this.timeouts.reloadCss = setTimeout(this.boundMethods.doReloadCss, 100);
    }

    /**
     * Handler that reloads all css on page
     *
     * @async
     * @return {undefined}
     */
    async doReloadCss () {
        clearTimeout(this.timeouts.reloadCss);
        try {
            await this.generateCss(true, true);
            await this.refreshCss();
        } catch (ex){
            this.log('Problem reloading CSS - "{1}"', 'error', [ex.message]);
        }
    }


    /**
     * Removes watchers from all watched files
     *
     * @async
     * @return {undefined}
     */
    async unwatchFiles () {
        for (let i=0; i<this.watchedFiles.length; i++){
            await _appWrapper.fileManager.unwatch(this.watchedFiles[i], this.boundMethods.cssFileChanged);
        }
        this.watchedFiles = [];
    }

    /**
     * Detects eventual missing css variables
     *
     * @param  {Boolean} silent Flag to prevent logging
     * @return {string[]}       All missing css variable names
     */
    async detectMissingVariables (silent){
        let missingVariables = [];
        if (!this.getConfig('compileCss')){
            this.log('Missing css variables detection only works with compiled css', 'info');
        } else {
            let styleHelper = _appWrapper.getHelper('style');
            let allVariables = [];
            let cssString = await this.compileCss(true, true);
            let matches = cssString.match(/var\((--[^)]+)/g);
            if (matches){
                let fixedMatches = _.map(matches, (item) => {
                    return item.replace(/var\(/gm, '');
                });
                allVariables = _.concat(allVariables, fixedMatches);
            }
            allVariables = _.uniq(allVariables);
            allVariables = allVariables.sort();
            for (let i=0; i<allVariables.length; i++){
                let value = styleHelper.getCssVarValue(allVariables[i]);
                if (!value){
                    missingVariables.push(allVariables[i]);
                }
            }
            if (!silent){
                if (missingVariables && missingVariables.length){
                    this.log('Missing css variables found - "{1}"', 'warning', [missingVariables.join('", "')]);
                } else {
                    this.log('No missing css variables found', 'info', []);
                }
            }
        }
        return missingVariables;
    }

    /**
     * Returns path to compiled css file (relative or absolute, based on parameter)
     *
     * @param  {Boolean} relativePath Flag to indicate that relative path should be returned
     * @return {string}               Compiled css file path
     */
    getCompiledCssPath (relativePath){
        let compiledCssPath = path.join(_appWrapper.getExecPath(), this.getConfig('varDir'), this.getConfig('appConfig.cssCompiledFile'));
        if (!relativePath){
            compiledCssPath = path.resolve(compiledCssPath);
        }
        return compiledCssPath;
    }
}

exports.StaticFilesHelper = StaticFilesHelper;