HEX
Server: Apache
System: Linux webd004.cluster130.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64
User: frenchy (106757)
PHP: 7.4.33
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/f/r/e/frenchy/www/french-american.org/current/node_modules/@frctl/fractal/src/web/builder.js
'use strict';

const Promise = require('bluebird');
const anymatch = require('anymatch');
const Path = require('path');
const co = require('co');
const _ = require('lodash');
const fs = Promise.promisifyAll(require('fs-extra'));
const Log = require('../core/log');
const mix = require('../core/mixins/mix');
const Emitter = require('../core/mixins/emitter');
const throat = require('throat');

module.exports = class Builder extends mix(Emitter) {

    constructor(theme, engine, config, app) {
        super(app);

        this._app = app;
        this._engine = engine;
        this._config = config;
        this._theme = theme;

        this._static = [];
        this._requests = [];

        this._throttle = throat(config.concurrency || 100);

        this._init();
    }

    /*
     * Deprecated. Use start() instead.
     */
    build() {
        return this.start();
    }

    start() {

        this._validate();
        this._reset();

        // Make sure the sources have loaded
        return this._app.load().then(() => {

            this._buildRequests();

            this.emit('start');

            // remove and recreate build dir
            const setup = fs.removeAsync(this._config.dest).then(() => fs.ensureDirAsync(this._config.dest));

            return setup.then(() => {

                let jobs = [];

                // 1. Start any static copy jobs
                jobs.push(this._static.map(p => this._throttle(() => this._copy(p.path, Path.join(Path.sep, p.mount)))));

                // 2. Run the requests in parallel
                this._requests.forEach(r => {
                    let req = this._throttle(() => this._onRequest(r));
                    if (req) {
                        this._jobsCount++;
                        jobs.push(req);
                    }
                });

                return Promise.all(_.flatten(jobs));
            });

        }).then(() => {
            const stats = {
                errorCount: this._errorCount,
            };
            this.emit('end', stats);
            return stats;
        }).catch(e => {
            this.emit('error', e);
            throw e;
        });
    }

    stop() {
        // can we stop it once running?
    }

    use() {

    }

    _reset() {
        this._errorCount = 0;
        this._jobsCount = 0;
        this._progressCount = 0;
    }

    _init() {
        this._static = this._static.concat(this._theme.static());
        this._engine.setGlobal('env', {
            builder: true,
        });
    }

    _buildRequests() {
        const routes = this._theme.routes();
        const resolvers = _.isFunction(this._theme.resolvers) ? this._theme.resolvers() : this._getLegacyResolvers(routes);
        let requests = [];

        _.forEach(resolvers, (routeResolvers, handle) => {
            const route = _.find(routes, {handle: handle});

            if (!route) {
                Log.debug(`No route found for handle '${handle}'`);
                return;
            }

            for (let resolver of routeResolvers) {
                let resolverSet = _.isFunction(resolver) ? resolver(this._app) : [].concat(resolver);
                for (const params of resolverSet) {
                    const url = this._theme.urlFromRoute(route.handle, params, true);
                    const req = Builder.Request(url, params, route);
                    this._requests.push(req);
                }
            }

        });
    }

    _getLegacyResolvers(routes) {
        let resolvers = {};
        for (let route of routes) {
            _.set(resolvers, route.handle, [].concat(route.params || null));
        }
        return resolvers;
    }

    _onRequest(req) {

        if (req.route.redirect && this._theme.redirectView) {
            req.route.context = {
                redirectUrl: req.route.redirect
            };
            req.route.view = this._theme.redirectView();
        }

        if (req.route.static) {
            const staticPath = _.isFunction(req.route.static) ? req.route.static(req.params, this._app) : req.route.static;
            return this._copy(unescape(staticPath), unescape(req.url), false);
        }

        if (req.route.view) {
            const ext = this._app.web.get('builder.ext');
            const dest = req.url + (req.url == '/' ? `index${ext}` : ext);
            const context = req.route.context || {};

            context.request = req;
            context.renderEnv = {
                request: context.request,
                builder: true,
                server: false,
            };

            return this._render(req.route.view, context)
                    .then(contents => this._write(contents, dest) )
                    .then(() => {
                        this.emit('exported', req);
                        Log.debug(`Exported '${req.url}' ==> '${dest}'`);
                        this._updateProgress();
                    })
                    .catch(err => this._onError(err, req, dest));
        }

    }

    _onError(err, req, dest) {
        this._errorCount++;
        this._updateProgress();
        this.emit('error', new Error(`Failed to export url ${req.url} - ${err.message}`));
        return this._render(this._theme.errorView(), { error: err }).then(contents => this._write(contents, dest)).catch(err => {
            this.emit('error', err);
        });
    }

    _render(view, context) {
        if (_.isFunction(view)) {
            return this._engine.renderString(view(), context);
        } else {
            return this._engine.render(view, context);
        }
    }

    _copy(source, dest, addToJobCount) {
        let ignored = this._config.static.ignored;
        dest = _.trimEnd(Path.join(this._config.dest, dest), Path.sep);
        source = Path.resolve(source);
        if (addToJobCount !== false) {
            this._jobsCount++;
        }
        return fs.copyAsync(source, dest, {
            clobber: true,
            filter: function(path){
                return ! anymatch(ignored, path);
            }
        }).then(() => {
            this._updateProgress();
            Log.debug(`Copied '${source}' ==> '${dest}'`);
        }).catch(e => {
            this._updateProgress();
            Log.error(`Error copying '${source}' ==> '${dest}'`);
            this._errorCount++;
        });
    }

    _write(contents, dest) {
        dest = _.trimEnd(Path.join(this._config.dest, dest), Path.sep);
        return fs.ensureDirAsync(Path.parse(dest).dir).then(() => fs.writeFileAsync(dest, contents));
    }

    _updateProgress() {
        this._progressCount++;
        this.emit('progress', this._progressCount, this._jobsCount);
    }

    _validate() {
        if (!this._config.dest) {
            throw new Error('You need to specify a build destination in your configuration.');
        }
        for (const stat of this._theme.static()) {
            if (stat.path == this._config.dest) {
                throw new Error(`Your build destination directory (${Path.resolve(stat.path)}) cannot be the same as any of your static assets directories.`);
            }
        }
    }

    static Request(url, params, route) {
        route = _.clone(route);
        return {
            headers: {},
            query: {},
            url: url,
            segments: _.compact(url.split('/')),
            params: params,
            path: url,
            error: null,
            errorStatus: null,
            route: route,
        };
    }

};