generate-static-site command


import { default as mimeTypes } from 'mime-types';

import { App, View } from 'blognami';

export default {
    async run(){

        const { extractOptions } = this.cliUtils;

        const { app = 'main' } = extractOptions();

        const { viewNames } = View.mapperFor(App.create(app, this.context).compose());

        this.pages = {};
        const urls = viewNames.filter(path => !path.match(/(^|\/)_/)).map(path => {
            return new URL(path, 'http://127.0.0.1/');
        });

        urls.push(new URL('http://127.0.0.1/404'));

        while(urls.length){
            await this.crawlPage({ _url: urls.shift(), _headers: { 'x-app': app } });
        }

        const pages = Object.values(this.pages).filter(page => {
            const { _method, _url, _headers, ...otherParams } = page.params;
            return _url.pathname == '/404' || (page.status == 200 && !Object.keys(otherParams).length)
        });

        const { inProjectRootDir, generateDir, generateFile, echo } = this.fsBuilder;

        const isGenerated = {};

        await inProjectRootDir(async () => {
            await generateDir('build/static', async () => {
                while(pages.length){
                    const { params, headers } = pages.shift();
                    const contentType = headers['content-type'];

                    const path = params._url.pathname;
                    let filePath = path.replace(/^\//, '');
                    if(filePath.match(/(^|\/)$/)){
                        filePath = `${filePath}index`;
                    }
                    if(!filePath.match(/[^/]+\.[^/]+$/)){
                        filePath = `${filePath}.${mimeTypes.extension(contentType)}`
                    }
                    
                    const data = (await this.fetch({ _url: new URL(path, 'http://127.0.0.1/'), _headers: { 'x-app': app } }))[2];

                    if(!isGenerated[filePath]){
                        isGenerated[filePath] = true;
                        await generateFile(filePath, () => {
                            echo(data.join(''));
                        });    
                    }
                }
            });
        });
    },

    async crawlPage(params){
        const hash = JSON.stringify(params);
        if(this.pages[hash]) return;
        const page = { params };
        this.pages[hash] = page;
        const [ status, headers, data ] = await this.fetch(params);
        page.status = status;
        page.headers = headers;
        if(status != 200 || headers['content-type'] != 'text/html') return;
        const html = data.join('');
        const virtualDom = this.parseHtml(html);
        const urls = this.extractUrls(virtualDom);
        while(urls.length){
            const url = urls.shift();
            await this.crawlPage({ ...url.params, _headers: params._headers, _url: url });
        }
    },

    extractUrls(virtualDom){
        const out = [];
        virtualDom.traverse(({ attributes }) => {
            ['src', 'href'].forEach(name => {
                const value = attributes[name];
                if(!value) return;
                const url = new URL(value, 'http://127.0.0.1/');
                if(url.host != '127.0.0.1') return;
                out.push(url);
            });
        });
        return out;
    }
};