Static site generator using JavaScript Promises I have created a small static site generator (for learning purposes) in Javascript ES6, using Promises, but I am not certain of how to use them well. The code below works fine, but I wonder if i can't write my promises a better way.

I call my promises in SSG.start();

const yaml = require('js-yaml');
const fs   = require('fs-extra');
const ejs  = require('ejs');
const md   = require('markdown-it')({
    html: true,
    linkify: true,
    typographer: true
});

let SSG = {
    outputFolder: '../output/',

    contentFolder: '../content/',

    assetsFolder: '../assets/',

    contentFiles: async function (dir, filelist) {
        return new Promise((resolve) =&gt; {
            files = fs.readdirSync(dir);
            filelist = filelist || [];
            files.forEach((file) =&gt; {
                if (fs.statSync(dir + '/' + file).isDirectory()) {
                    return resolve(contentFiles(dir + '/' + file, filelist));
                } else {
                    filelist.push(file);
                    return resolve(filelist);
                }
            });
        })
    },

    generateHTMLFile: async function (file) {
        return new Promise((resolve) =&gt; {
            let fstream = fs.readFileSync(file, 'utf8');
            let params = fstream.match(/\+Params\+([\s\S]*?)\+\+\+/g);
            let content = fstream.match(/(\+Content[0-9]*\+|\+Content\+)([\s\S]*?)*(\+\+\+)/g);
            let contents = [];

            params = params[0].replace('+Params+', '').replace('+++', '');
            content = content[0].split('+++');

            try {
                params = yaml.safeLoad(params, 'utf8');
                for (it of content) {
                    it = it.replace('+++', '').replace(/(\+Content[0-9]*\+|\+Content\+)/g, '');
                    contents.push(md.render(it));
                }

                const funs = require('./functions.js');

                ejs.renderFile('../templates/' + params.template + '.ejs', {funs: funs, params: params, content: contents}, {}, (err, html) =&gt; {
                    if (err) console.log(err);
                    resolve({html: html, url: params.url});
                });

            } catch (e) {
                console.log(e);
            }
        });
    },

    saveHTML: function (html, url) {
        if (!fs.existsSync(this.outputFolder)) {
            fs.mkdir(this.outputFolder, { recursive: true }, (err) =&gt; {
                if (err) throw err;
            });
        }
        fs.writeFile(this.outputFolder + url + '.html', html, (err) =&gt; {
            if (err)  return console.log(err);
        });
    },

    saveStaticAssets: function () {
        fs.copySync(this.assetsFolder, this.outputFolder);
    },

    start: async function () {
        try {
            let time = Date.now();
            console.log('Génération des fichiers HTML...');

            let files = await this.contentFiles(this.contentFolder);

            for (file of files) {
                await this.generateHTMLFile(this.contentFolder + file).then(({html, url}) =&gt; {
                    this.saveHTML(html, url);
                });
            }

            this.saveStaticAssets();

            console.log('Généré en ' + (Date.now() - time) + 'ms');
        } catch (e) {
            console.error(e.message);
        }
    }
};

(async () =&gt; {
    await SSG.start();
})();



What do you think about it ?
First thing that bother me is why let SSG? Why is it an object? If you're using ES6+ (as you do), I'd go with making this a class. It's more obvious and more readable. Note that while doing this, you'll have to alter the function signature, i.e. generateHtmlFile: async function (file) { ... } becomes async generateHtmlFile(file) { ... }.

Since you have folder names stored as a properties of your object (outputFolder, contentFolder and assetsFolder), they should be moved now to the constructor. There is also a good candidate for another new property on line 51: ../templates/ should be also extracted. This is because there are no properties in JS class ATM. Also, while we're on the topic of class properties, I'd suggest that you should pass an object with folder names as a constructor argument. That way you can easily change the folders you're using by just instantiating an object with another folder names. When we sum up the info about properties, here's what it should look like:

class SGG {
  constructor(folders = {}) {
    this.outputFolder = folders.outputFolder || '../output/'
    this.contentFolder = folders.contentFolder || '../content/'
    this.assetsFolder = folders.assetsFolder || '../assets/'
    this.templates = folders.templates || '../templates/'
  }
}


The second thing that I didn't notice when I looked at the code here is that you have a some implicit variables: files on line 19 and it on line 45. So just add const in front of those two and you'll be fine.

At line 49, there is a require. I'd move it to the top of the file because that way you know right on what are the dependencies of your file. And it's more cleaner code without some unexpected turns.

Now, to the methods. First one is contentFiles which is just a recursive call to find out all the files in a contentFolder directory. I see that you are probably confused with promises because you use async and new Promise in the same function. When making function an async, you should know that whatever you return from the function, it will be wrapped inside a promise and it also gives you the ability to use await inside it. But since you don't have await here, then there is no need for making this function an async one. You are already returning a promise. But, I think that you don't need a promise at all! You are using these fs-extra methods that are not returning a promise nor have a callback, so they are perfectly synchronous functions. Also, the .forEach got all of this tangled a bit, so if you remove it and use for..of, you can untangle yourself and get a cleaner code.

The next method, generateHTMLFile has similar issue with promises as the one above. From ejs' github, I noticed that if you don't pass the callback function, it will return a promise. So what should be done here is to remove new Promise... thing and keep async and rewrite the ejs.renderFiles not to use the callback function.

The method saveHTML is confusing a bit: you're using sync methods everywhere, but here you decided to go with mkdir instead of mkdirSync or writeFileSync.

Once you apply all of what I've said, you should get something like this:

const yaml = require('js-yaml');
const fs = require('fs-extra');
const ejs = require('ejs');
const md = require('markdown-it')({
    html: true,
    linkify: true,
    typographer: true,
});
const funs = require('./functions.js');

class SSG {
    constructor(folders = {}) {
        this.outputFolder = folders.output || '../output/'
        this.contentFolder = folders.contentFolder || '../content/'
        this.assetsFolder = folders.assetsFolder || '../assets/'
        this.templateFolder = folders.templateFolder || '../templates/'
    }

    contentFiles(dir, filelist) {
        const files = fs.readdirSync(dir)
        filelist = filelist || []
        for (const file of files) {
            if (fs.statSync(dir + '/' + file).isDirectory()) {
                contentFiles(dir + '/' + file, filelist)
            } else {
                filelist.push(file)
            }
        }
        return filelist
    }

    async generateHTMLFile(file) {
        const fstream = fs.readFileSync(file, 'utf8');
        let params = fstream.match(/\+Params\+([\s\S]*?)\+\+\+/g);
        let content = fstream.match(/(\+Content[0-9]*\+|\+Content\+)([\s\S]*?)*(\+\+\+)/g);
        let contents = [];

        params = params[0].replace('+Params+', '').replace('+++', '');
        content = content[0].split('+++');

        try {
            params = yaml.safeLoad(params, 'utf8');
            for (let it of content) {
                it = it.replace('+++', '').replace(/(\+Content[0-9]*\+|\+Content\+)/g, '');
                contents.push(md.render(it));
            }

             const html = await ejs.renderFile(this.templateFolder + params.template + '.ejs', {
                funs: funs,
                params: params,
                content: contents,
            }, {})

            return {
                html, 
                url: params.url,
            }

        } catch (e) {
            console.log(e);
        }
    }

    async saveHTML(html, url) {
        if (!fs.existsSync(this.outputFolder)) {
            await fs.mkdirSync(this.outputFolder, {recursive: true});
        }
        await fs.writeFileSync(this.outputFolder + url + '.html', html);
    }

    saveStaticAssets() {
        fs.copySync(this.assetsFolder, this.outputFolder);
    }

    async start() {
        try {
            let time = Date.now();
            console.log('Génération des fichiers HTML...');

            const files = this.contentFiles(this.contentFolder);

            for (const file of files) {
                await this.generateHTMLFile(this.contentFolder + file).then(({html, url}) =&gt; {
                    this.saveHTML(html, url);
                });
            }

            this.saveStaticAssets();

            console.log('Généré en ' + (Date.now() - time) + 'ms');
        } catch (e) {
            console.error(e.message);
        }
    }
};

(async () =&gt; {
    const ssg = new SSG()
    await ssg.start();
})();


Last note that I'd like to make is that Node already has a pretty decent file system utilities. The only thing that I've noticed that is different from fs-extra is that sync functions do not return promises, but they rather act as a regular functions. Anyhow, I'd argue that you don't need fs-extra and use fs instead. But it's up to you.

I maybe missed something or maybe got into details and nitpicking more than needed, but this is a basic refactor that I would do to your code. I hope that I didn't break some things while doing this :)



Edit:
While looking at the code for the second time, I noticed that I've missed this line in start:

await this.generateHTMLFile(this.contentFolder + file).then(({html, url}) =&gt; {
    this.saveHTML(html, url);
});`


It's maybe just me, but I don't like this mixture of await and then.
I'd just suggest that if you're going with await, keep it that way.
My suggestion here would be to go with something like this:

const {html, url} = await this.generateHTMLFile(this.contentFolder + file)
await this.saveHTML(html, url)


I've also just noticed that you can optimize the solution for speed by awaiting on multiple promises:

await Promise.all(files.map(async (file) =&gt; {
    const {html, url} = await this.generateHTMLFile(this.contentFolder + file)
    await this.saveHTML(html, url)
}))


This way you start multiple promises in parallel (again, not a true parallelization since JS is single threaded) and await on all of them to finish before moving on. This way you'll get a bit faster execution. Note that if one promise throws and error, this line will also throw an error before waiting on the other promises to end.